Read receipts: create custom view to use it wherever we want easily

This commit is contained in:
ganfra 2019-08-08 17:51:06 +02:00
parent 825463d9cd
commit 1dbb02a80d
8 changed files with 153 additions and 104 deletions

View file

@ -0,0 +1,77 @@
/*
* 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.riotx.core.ui.views
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.core.view.isVisible
import butterknife.ButterKnife
import im.vector.riotx.R
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import kotlinx.android.synthetic.main.view_read_receipts.view.*
private const val MAX_RECEIPT_DISPLAYED = 5
class ReadReceiptsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private val receiptAvatars: List<ImageView> by lazy {
listOf(receiptAvatar1, receiptAvatar2, receiptAvatar3, receiptAvatar4, receiptAvatar5)
}
init {
setupView()
}
private fun setupView() {
inflate(context, R.layout.view_read_receipts, this)
ButterKnife.bind(this)
}
fun render(readReceipts: List<ReadReceiptData>, avatarRenderer: AvatarRenderer) {
if (readReceipts.isNotEmpty()) {
isVisible = true
for (index in 0 until MAX_RECEIPT_DISPLAYED) {
val receiptData = readReceipts.getOrNull(index)
if (receiptData == null) {
receiptAvatars[index].isVisible = false
} else {
receiptAvatars[index].isVisible = true
avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, receiptAvatars[index])
}
}
if (readReceipts.size > MAX_RECEIPT_DISPLAYED) {
receiptMore.isVisible = true
receiptMore.text = context.getString(
R.string.x_plus, readReceipts.size - MAX_RECEIPT_DISPLAYED
)
} else {
receiptMore.isVisible = false
}
} else {
isVisible = false
}
}
}

View file

@ -25,23 +25,18 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.senderName
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem
import im.vector.riotx.features.home.room.detail.timeline.item.NoticeItem_
import im.vector.riotx.features.home.room.detail.timeline.util.MessageInformationDataFactory
import javax.inject.Inject
class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter,
private val avatarRenderer: AvatarRenderer) {
private val avatarRenderer: AvatarRenderer,
private val informationDataFactory: MessageInformationDataFactory) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?): NoticeItem? {
val formattedText = eventFormatter.format(event) ?: return null
val informationData = MessageInformationData(
eventId = event.root.eventId ?: "?",
senderId = event.root.senderId ?: "",
sendState = event.root.sendState,
avatarUrl = event.senderAvatar(),
memberName = event.senderName(),
showInformation = false
)
val informationData = informationDataFactory.create(event, null)
return NoticeItem_()
.avatarRenderer(avatarRenderer)

View file

@ -33,6 +33,7 @@ import com.airbnb.epoxy.EpoxyAttribute
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.ui.views.ReadReceiptsView
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.core.utils.DimensionUtils.dpToPx
import im.vector.riotx.features.home.AvatarRenderer
@ -40,8 +41,6 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle
import im.vector.riotx.features.reactions.widget.ReactionButton
import im.vector.riotx.features.ui.getMessageTextColor
private const val MAX_RECEIPT_DISPLAYED = 5
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
@EpoxyAttribute
@ -125,28 +124,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
holder.memberNameView.setOnLongClickListener(null)
}
if (informationData.readReceipts.isNotEmpty()) {
holder.readReceiptsView.isVisible = true
for (index in 0 until MAX_RECEIPT_DISPLAYED) {
val receiptData = informationData.readReceipts.getOrNull(index)
if (receiptData == null) {
holder.receiptAvatars[index].isVisible = false
} else {
holder.receiptAvatars[index].isVisible = true
avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, holder.receiptAvatars[index])
}
}
if (informationData.readReceipts.size > MAX_RECEIPT_DISPLAYED) {
holder.receiptMoreView.isVisible = true
holder.receiptMoreView.text = holder.view.context.getString(
R.string.x_plus, informationData.readReceipts.size - MAX_RECEIPT_DISPLAYED
)
} else {
holder.receiptMoreView.isVisible = false
}
} else {
holder.readReceiptsView.isVisible = false
}
holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer)
if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) {
holder.reactionWrapper?.isVisible = false
@ -198,17 +176,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
val timeView by bind<TextView>(R.id.messageTimeView)
val readReceiptsView by bind<ViewGroup>(R.id.readReceiptsView)
val receiptAvatar1 by bind<ImageView>(R.id.message_avatar_receipt_1)
val receiptAvatar2 by bind<ImageView>(R.id.message_avatar_receipt_2)
val receiptAvatar3 by bind<ImageView>(R.id.message_avatar_receipt_3)
val receiptAvatar4 by bind<ImageView>(R.id.message_avatar_receipt_4)
val receiptAvatar5 by bind<ImageView>(R.id.message_avatar_receipt_5)
val receiptMoreView by bind<TextView>(R.id.message_more_than_expected)
val receiptAvatars: List<ImageView> by lazy {
listOf(receiptAvatar1, receiptAvatar2, receiptAvatar3, receiptAvatar4, receiptAvatar5)
}
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
var reactionWrapper: ViewGroup? = null
var reactionFlowHelper: Flow? = null
}

View file

@ -22,6 +22,7 @@ import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.ui.views.ReadReceiptsView
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@ -55,6 +56,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
holder.avatarImageView
)
holder.view.setOnLongClickListener(longClickListener)
holder.readReceiptsView.render(informationData.readReceipts, avatarRenderer)
}
override fun getViewType() = STUB_ID
@ -62,6 +64,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
class Holder : BaseHolder(STUB_ID) {
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
val noticeTextView by bind<TextView>(R.id.itemNoticeTextView)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
}
companion object {

View file

@ -123,65 +123,14 @@
</ViewStub>
<LinearLayout
<im.vector.riotx.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:orientation="horizontal"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/message_more_than_expected"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:gravity="center"
android:textSize="12sp"
tools:text="999+" />
<ImageView
android:id="@+id/message_avatar_receipt_5"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/message_avatar_receipt_4"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/message_avatar_receipt_3"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/message_avatar_receipt_2"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/message_avatar_receipt_1"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
</LinearLayout>
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -52,5 +52,14 @@
android:layout="@layout/item_timeline_event_merged_header_stub"
tools:ignore="MissingConstraints" />
<im.vector.riotx.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,11 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
android:id="@+id/readReceiptsView"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:parentTag="android.widget.LinearLayout">
<TextView
android:id="@+id/receiptMore"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:gravity="center"
android:textSize="12sp"
tools:text="999+" />
<ImageView
android:id="@+id/receiptAvatar5"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
</RelativeLayout>
<ImageView
android:id="@+id/receiptAvatar4"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/receiptAvatar3"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/receiptAvatar2"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/receiptAvatar1"
android:layout_width="16dp"
android:layout_height="16dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
</merge>

View file

@ -297,6 +297,7 @@
<style name="TimelineContentStubNoInfoLayoutParams" parent="TimelineContentStubBaseParams">
<item name="layout_constraintTop_toTopOf">parent</item>
<item name="layout_constraintBottom_toTopOf">@id/readReceiptsView</item>
</style>