[bubble merge] add back SC bubbles

Change-Id: Ia884fbc5d83aad8cb79d674ec7c411bff608e4e5
This commit is contained in:
SpiritCroc 2022-02-19 20:36:41 +01:00
parent 1024761526
commit ef4c35499c
26 changed files with 1071 additions and 135 deletions

View File

@ -1,11 +1,26 @@
package im.vector.app.core.ui.views
import android.content.Context
import android.view.ViewGroup
import androidx.core.view.children
import im.vector.app.features.themes.BubbleThemeUtils
import android.content.res.Resources
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
interface BubbleDependentView<H> {
interface BubbleDependentView<H: VectorEpoxyHolder> {
fun getScBubbleMargin(resources: Resources): Int = resources.getDimensionPixelSize(R.dimen.dual_bubble_one_side_without_avatar_margin)
fun getViewStubMinimumWidth(holder: H): Int = 0
fun allowFooterOverlay(holder: H, bubbleWrapView: ScMessageBubbleWrapView): Boolean = false
// Whether to show the footer aligned below the viewStub - requires enough width!
fun allowFooterBelow(holder: H): Boolean = true
fun needsFooterReservation(): Boolean = false
fun reserveFooterSpace(holder: H, width: Int, height: Int) {}
fun getInformationData(): MessageInformationData? = null
// TODO: overwrite for remaining setBubbleLayout()s where necessary: ReadReceiptsItem, MessageImageVideoItem
fun applyScBubbleStyle(messageLayout: TimelineMessageLayout.ScBubble, holder: H) {}
/*
fun messageBubbleAllowed(context: Context): Boolean {

View File

@ -30,6 +30,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEve
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
@ -46,6 +47,7 @@ import javax.inject.Inject
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val avatarRenderer: AvatarRenderer,
private val avatarSizeProvider: AvatarSizeProvider,
private val messageLayoutFactory: TimelineMessageLayoutFactory,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
private val collapsedEventIds = linkedSetOf<Long>()
@ -129,7 +131,8 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
onCollapsedStateChanged = {
mergeItemCollapseStates[event.localId] = it
requestModelBuild()
}
},
messageLayout = messageLayoutFactory.createDummy()
)
MergedMembershipEventsItem_()
.id(mergeId)
@ -206,6 +209,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
mergeItemCollapseStates[event.localId] = it
requestModelBuild()
},
messageLayout = messageLayoutFactory.createDummy(),
hasEncryptionEvent = hasEncryption,
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
callback = callback,

View File

@ -21,10 +21,12 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem_
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import javax.inject.Inject
class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer) {
class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer,
private val messageLayoutFactory: TimelineMessageLayoutFactory) {
fun create(
eventId: String,
@ -44,6 +46,7 @@ class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: Av
.id("read_receipts_$eventId")
.eventId(eventId)
.readReceipts(readReceiptsData)
.messageLayout(messageLayoutFactory.createDummy())
.avatarRenderer(avatarRenderer)
.shouldHideReadReceipts(isFromThreadTimeLine)
.clickListener {

View File

@ -161,16 +161,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
ReferencesInfoData(verificationState)
},
sentByMe = isSentByMe,
readReceiptAnonymous = if (event.root.sendState == SendState.SYNCED || event.root.sendState == SendState.SENT) {
/*if (event.readByOther) {
AnonymousReadReceipt.READ
} else {
AnonymousReadReceipt.SENT
}*/
AnonymousReadReceipt.NONE
} else {
AnonymousReadReceipt.PROCESSING
},
readReceiptAnonymous = BubbleThemeUtils.anonymousReadReceiptForEvent(event),
senderPowerLevel = senderPowerLevel,
isDirect = isEffectivelyDirect,
isPublic = roomSummary?.isPublic ?: false,

View File

@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail.timeline.item
import android.content.res.Resources
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
@ -25,13 +26,19 @@ import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.ui.views.BubbleDependentView
import im.vector.app.core.ui.views.ShieldImageView
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
import im.vector.app.features.home.room.detail.timeline.view.scRenderMessageLayout
import im.vector.app.features.reactions.widget.ReactionButton
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.room.send.SendState
import kotlin.math.ceil
/**
* Base timeline item with reactions and read receipts.
@ -85,23 +92,22 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
}
// SchildiChat: moved to setBubbleLayout() (called from super.bind()) - so we can do this bubble-style-specific
/*
when (baseAttributes.informationData.e2eDecoration) {
E2EDecoration.NONE -> {
holder.e2EDecorationView.render(null)
}
E2EDecoration.WARN_IN_CLEAR,
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
if (baseAttributes.informationData.messageLayout.showE2eDecoration) {
when (baseAttributes.informationData.e2eDecoration) {
E2EDecoration.NONE -> {
holder.e2EDecorationView.render(null)
}
E2EDecoration.WARN_IN_CLEAR,
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
}
}
}
*/
holder.view.onClick(baseAttributes.itemClickListener)
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
(holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout)
(holder.view as? TimelineMessageLayoutRenderer).scRenderMessageLayout(baseAttributes.informationData.messageLayout, this, holder)
}
override fun unbind(holder: H) {
@ -116,6 +122,25 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
failureIndicator?.isVisible = baseAttributes.informationData.sendState.hasFailed()
}
override fun getScBubbleMargin(resources: Resources): Int {
return when {
(baseAttributes.informationData.messageLayout as? TimelineMessageLayout.ScBubble)?.singleSidedLayout == true -> 0
// else: dual-side bubbles (getBubbleMargin should not get called for other bubbleStyles)
// Direct chats usually have avatars hidden on both sides
baseAttributes.informationData.isDirect -> resources.getDimensionPixelSize(R.dimen.dual_bubble_both_sides_without_avatar_margin)
// No direct chat, but sent by me: other side has an avatar
baseAttributes.informationData.sentByMe -> {
resources.getDimensionPixelSize(R.dimen.dual_bubble_one_side_without_avatar_margin) +
resources.getDimensionPixelSize(R.dimen.dual_bubble_one_side_avatar_offset) +
// SC bubbles use SMALL avatars
ceil(AvatarSizeProvider.Companion.AvatarStyle.SMALL.avatarSizeDP * resources.displayMetrics.density).toInt()
}
// No direct chat, sent by other: my side has hidden avatar
else -> resources.getDimensionPixelSize(R.dimen.dual_bubble_one_side_without_avatar_margin)
}
}
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
val reactionsContainer by bind<ViewGroup>(R.id.reactionsContainer)
val informationBottom by bind<ViewGroup>(R.id.informationBottom)

View File

@ -36,8 +36,15 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
import im.vector.app.features.home.room.detail.timeline.view.canHideAvatars
import im.vector.app.features.home.room.detail.timeline.view.infoInBubbles
import im.vector.app.features.themes.guessTextWidth
import org.matrix.android.sdk.api.session.threads.ThreadDetails
import org.matrix.android.sdk.api.util.MatrixItem
import kotlin.math.ceil
/**
* Base timeline item that adds an optional information bar with the sender avatar, name, time, send state
@ -75,40 +82,42 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
override fun bind(holder: H) {
super.bind(holder)
if (attributes.informationData.messageLayout.showAvatar) {
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
height = attributes.avatarSize
width = attributes.avatarSize
if ((holder.view as? ScMessageBubbleWrapView)?.customBind(this, holder, attributes, _avatarClickListener, _memberNameClickListener) != true) {
if (attributes.informationData.messageLayout.showAvatar) {
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
height = attributes.avatarSize
width = attributes.avatarSize
}
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener)
holder.avatarImageView.isVisible = true
holder.avatarImageView.onClick(_avatarClickListener)
} else {
holder.avatarImageView.setOnClickListener(null)
holder.avatarImageView.setOnLongClickListener(null)
holder.avatarImageView.isVisible = false
}
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener)
holder.avatarImageView.isVisible = true
holder.avatarImageView.onClick(_avatarClickListener)
} else {
holder.avatarImageView.setOnClickListener(null)
holder.avatarImageView.setOnLongClickListener(null)
holder.avatarImageView.isVisible = false
if (attributes.informationData.messageLayout.showDisplayName) {
holder.memberNameView.isVisible = true
holder.memberNameView.text = attributes.informationData.memberName
holder.memberNameView.setTextColor(attributes.getMemberNameColor())
holder.memberNameView.onClick(_memberNameClickListener)
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
} else {
holder.memberNameView.setOnClickListener(null)
holder.memberNameView.setOnLongClickListener(null)
holder.memberNameView.isVisible = false
}
if (attributes.informationData.messageLayout.showTimestamp) {
holder.timeView.isVisible = true
holder.timeView.text = attributes.informationData.time
} else {
holder.timeView.isVisible = false
}
// Render send state indicator
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
}
if (attributes.informationData.messageLayout.showDisplayName) {
holder.memberNameView.isVisible = true
holder.memberNameView.text = attributes.informationData.memberName
holder.memberNameView.setTextColor(attributes.getMemberNameColor())
holder.memberNameView.onClick(_memberNameClickListener)
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
} else {
holder.memberNameView.setOnClickListener(null)
holder.memberNameView.setOnLongClickListener(null)
holder.memberNameView.isVisible = false
}
if (attributes.informationData.messageLayout.showTimestamp) {
holder.timeView.isVisible = true
holder.timeView.text = attributes.informationData.time
} else {
holder.timeView.isVisible = false
}
// Render send state indicator
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
// Threads
if (attributes.areThreadMessagesEnabled) {
@ -151,14 +160,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
super.unbind(holder)
}
private fun Attributes.getMemberNameColor() = messageColorProvider.getMemberNameTextColor(
informationData.matrixItem,
MatrixItemColorProvider.UserInRoomInformation(
attributes.informationData.isDirect,
attributes.informationData.isPublic,
attributes.informationData.senderPowerLevel
)
)
override fun getInformationData(): MessageInformationData? = attributes.informationData
abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) {
@ -216,10 +218,20 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
return result
}
fun getMemberNameColor() = messageColorProvider.getMemberNameTextColor(
informationData.matrixItem,
MatrixItemColorProvider.UserInRoomInformation(
informationData.isDirect,
informationData.isPublic,
informationData.senderPowerLevel
)
)
}
override fun ignoreMessageGuideline(context: Context): Boolean {
return false // SC-TODO infoInBubbles(context) && canHideAvatars()
val messageLayout = attributes.informationData.messageLayout as? TimelineMessageLayout.ScBubble ?: return false
return infoInBubbles(messageLayout) && canHideAvatars(attributes)
}
}

View File

@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.content.Context
import android.view.View
import android.view.ViewStub
import android.widget.FrameLayout
import android.widget.RelativeLayout
import androidx.annotation.CallSuper
import androidx.annotation.IdRes
@ -28,6 +29,7 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.platform.CheckableView
import im.vector.app.core.ui.views.BubbleDependentView
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
/**
* Children must override getViewType()
@ -71,6 +73,7 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
val leftGuideline by bind<View>(R.id.messageStartGuideline)
val contentContainer by bind<View>(R.id.viewStubContainer)
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
val viewStubContainer by bind<FrameLayout>(R.id.viewStubContainer)
override fun bindView(itemView: View) {
super.bindView(itemView)

View File

@ -21,6 +21,9 @@ import android.widget.TextView
import androidx.annotation.IdRes
import im.vector.app.R
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
import im.vector.app.features.home.room.detail.timeline.view.scOnlyRenderMessageLayout
import org.matrix.android.sdk.api.util.MatrixItem
abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>() {
@ -39,6 +42,8 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
holder.separatorView.visibility = View.VISIBLE
holder.expandView.setText(R.string.merged_events_collapse)
}
(holder.view as? TimelineMessageLayoutRenderer).scOnlyRenderMessageLayout(attributes.messageLayout, this, holder)
}
protected val distinctMergeData by lazy {
@ -69,6 +74,7 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
val mergeData: List<Data>
val avatarRenderer: AvatarRenderer
val onCollapsedStateChanged: (Boolean) -> Unit
val messageLayout: TimelineMessageLayout
}
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {

View File

@ -23,6 +23,8 @@ import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
import im.vector.app.features.home.room.detail.timeline.view.scOnlyRenderMessageLayout
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
@ -35,6 +37,8 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
holder.messageTextView.text = attributes.text
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
(holder.view as? TimelineMessageLayoutRenderer).scOnlyRenderMessageLayout(attributes.informationData.messageLayout, this, holder)
}
override fun unbind(holder: Holder) {

View File

@ -25,6 +25,7 @@ import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
@ -69,6 +70,7 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
override val isCollapsed: Boolean,
override val mergeData: List<Data>,
override val avatarRenderer: AvatarRenderer,
override val onCollapsedStateChanged: (Boolean) -> Unit
override val onCollapsedStateChanged: (Boolean) -> Unit,
override val messageLayout: TimelineMessageLayout
) : BasedMergedItem.Attributes
}

View File

@ -37,6 +37,7 @@ import im.vector.app.core.utils.tappableMatchingText
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.tools.linkify
import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@ -224,6 +225,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
override val mergeData: List<Data>,
override val avatarRenderer: AvatarRenderer,
override val onCollapsedStateChanged: (Boolean) -> Unit,
override val messageLayout: TimelineMessageLayout,
val callback: TimelineEventController.Callback? = null,
val currentUserId: String,
val hasEncryptionEvent: Boolean,

View File

@ -33,6 +33,9 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadSt
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.themes.guessTextWidth
import kotlin.math.ceil
import kotlin.math.max
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
@ -103,6 +106,18 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
contentDownloadStateTrackerBinder.unbind(mxcUrl)
}
override fun getViewStubMinimumWidth(holder: Holder): Int {
// Guess text width for name and time
// On first call, holder.fileImageView.width is not initialized yet
val imageWidth = holder.fileImageView.resources.getDimensionPixelSize(R.dimen.chat_avatar_size)
val minimumWidthWithText =
ceil(guessTextWidth(holder.filenameView, filename)).toInt() +
imageWidth +
holder.filenameView.resources.getDimensionPixelSize(R.dimen.sc_bubble_guess_minimum_width_padding)
val absoluteMinimumWidth = imageWidth*3
return max(absoluteMinimumWidth, minimumWidthWithText)
}
override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {

View File

@ -16,9 +16,11 @@
package im.vector.app.features.home.room.detail.timeline.item
import android.content.res.Resources
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
@ -33,6 +35,7 @@ import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
import im.vector.app.features.media.ImageContentRenderer
import org.matrix.android.sdk.api.util.MimeTypes
@ -57,7 +60,6 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
@EpoxyAttribute
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
// SC-TODO
var lastAllowedFooterOverlay: Boolean = true
var lastShowFooterBellow: Boolean = true
var forceAllowFooterOverlay: Boolean? = null
@ -67,18 +69,19 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
forceAllowFooterOverlay = null
super.bind(holder)
val bubbleWrapView = (holder.view as? ScMessageBubbleWrapView)
val host = this
val onImageSizeListener = object: ImageContentRenderer.OnImageSizeListener {
override fun onImageSizeUpdated(width: Int, height: Int) {
bubbleWrapView ?: return
// Image size change -> different footer space situation possible
/* SC-TODO
val footerMeasures = getFooterMeasures(holder)
val footerMeasures = bubbleWrapView.getFooterMeasures(attributes.informationData)
forceAllowFooterOverlay = shouldAllowFooterOverlay(footerMeasures, width, height)
val newShowFooterBellow = shouldShowFooterBellow(footerMeasures, width, height)
if (lastAllowedFooterOverlay != forceAllowFooterOverlay || newShowFooterBellow != lastShowFooterBellow) {
showFooterBellow = newShowFooterBellow
updateMessageBubble(holder.imageView.context, holder)
bubbleWrapView.renderMessageLayout(attributes.informationData.messageLayout, host, holder)
}
*/
}
}
val animate = mediaData.mimeType == MimeTypes.Gif
@ -87,11 +90,10 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
val messageLayout = baseAttributes.informationData.messageLayout
val dimensionConverter = DimensionConverter(holder.view.resources)
// SC-TODO handle SC bubbles
val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
messageLayout.cornersRadius.granularRoundedCorners()
} else {
RoundedCorners(dimensionConverter.dpToPx(8))
val imageCornerTransformation = when (messageLayout) {
is TimelineMessageLayout.ScBubble -> RoundedCorners(dimensionConverter.dpToPx(3))
is TimelineMessageLayout.Bubble -> messageLayout.cornersRadius.granularRoundedCorners()
else -> RoundedCorners(dimensionConverter.dpToPx(8))
}
imageContentRenderer.render(mediaData, effectiveMode, holder.imageView, imageCornerTransformation, onImageSizeListener)
if (!attributes.informationData.sendState.hasFailed()) {
@ -122,7 +124,6 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
override fun getViewStubId() = STUB_ID
// SC-TODO
private fun shouldAllowFooterOverlay(footerMeasures: Array<Int>, imageWidth: Int, imageHeight: Int): Boolean {
val footerWidth = footerMeasures[0]
val footerHeight = footerMeasures[1]
@ -131,7 +132,6 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
return imageWidth > 1.5*footerWidth && imageHeight > 1.5*footerHeight && (imageWidth * imageHeight > 4 * footerWidth * footerHeight)
}
// SC-TODO
private fun shouldShowFooterBellow(footerMeasures: Array<Int>, imageWidth: Int, imageHeight: Int): Boolean {
// Only show footer bellow if the width is not the limiting factor (or it will get cut).
// Otherwise, we can not be sure in this place that we'll have enough space on the side
@ -141,6 +141,57 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
return imageWidth > 1.5*footerWidth && imageHeight < 1.5*footerHeight
}
override fun allowFooterOverlay(holder: Holder, bubbleWrapView: ScMessageBubbleWrapView): Boolean {
val rememberedAllowFooterOverlay = forceAllowFooterOverlay
if (rememberedAllowFooterOverlay != null) {
lastAllowedFooterOverlay = rememberedAllowFooterOverlay
return rememberedAllowFooterOverlay
}
val imageWidth = holder.imageView.width
val imageHeight = holder.imageView.height
if (imageWidth == 0 && imageHeight == 0) {
// Not initialised yet, assume true
lastAllowedFooterOverlay = true
return true
}
// If the footer covers most of the image, or is even larger than the image, move it outside
val footerMeasures = bubbleWrapView.getFooterMeasures(baseAttributes.informationData)
lastAllowedFooterOverlay = shouldAllowFooterOverlay(footerMeasures, imageWidth, imageHeight)
return lastAllowedFooterOverlay
}
override fun allowFooterBelow(holder: Holder): Boolean {
val showBellow = showFooterBellow
lastShowFooterBellow = showBellow
return showBellow
}
override fun getScBubbleMargin(resources: Resources): Int {
return 0
}
override fun applyScBubbleStyle(messageLayout: TimelineMessageLayout.ScBubble, holder: Holder) {
// Case: ImageContentRenderer.processSize only sees width=height=0 -> width of the ImageView not adapted to the actual content
// -> Align image within ImageView to same side as message bubbles
holder.imageView.scaleType = if (messageLayout.reverseBubble) ImageView.ScaleType.FIT_END else ImageView.ScaleType.FIT_START
// Case: Message information (sender name + date) makes the containing view wider than the ImageView
// -> Align ImageView within its parent to the same side as message bubbles
(holder.imageView.layoutParams as ConstraintLayout.LayoutParams).horizontalBias = if (messageLayout.reverseBubble) 1f else 0f
// Image outline
when {
!(messageLayout.isRealBubble || messageLayout.isPseudoBubble) || mode != ImageContentRenderer.Mode.THUMBNAIL -> {
// Don't show it for non-bubble layouts, don't show for Stickers, ...
holder.mediaContentView.background = null
}
attributes.informationData.sentByMe -> {
holder.mediaContentView.setBackgroundResource(R.drawable.background_image_border_outgoing)
}
else -> {
holder.mediaContentView.setBackgroundResource(R.drawable.background_image_border_incoming)
}
}
}
class Holder : AbsMessageItem.Holder(STUB_ID) {
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)

View File

@ -35,6 +35,7 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlUiState
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlViewSc
import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
import im.vector.app.features.media.ImageContentRenderer
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import io.noties.markwon.MarkwonPlugin
@ -174,6 +175,32 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
}
}
override fun allowFooterOverlay(holder: Holder, bubbleWrapView: ScMessageBubbleWrapView): Boolean {
return true
}
override fun needsFooterReservation(): Boolean {
return true
}
override fun reserveFooterSpace(holder: Holder, width: Int, height: Int) {
// Remember for PreviewUrlViewUpdater.onStateUpdated
footerWidth = width
footerHeight = height
// Reserve both in preview and in message
// User might close preview, so we still need place in the message
// if we don't want to change this afterwards
// This might be a race condition, but the UI-isssue if evaluated wrongly is negligible
if (!holder.previewUrlView.isVisible) {
holder.messageView.footerWidth = width
holder.messageView.footerHeight = height
} // else: will be handled in onStateUpdated
holder.previewUrlViewSc.footerWidth = height
holder.previewUrlViewSc.footerHeight = height
holder.previewUrlViewSc.updateFooterSpace()
}
companion object {
private const val STUB_ID = R.id.messageContentTextStub
}

View File

@ -27,6 +27,8 @@ import im.vector.app.core.epoxy.onClick
import im.vector.app.core.ui.views.ShieldImageView
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
import im.vector.app.features.home.room.detail.timeline.view.scOnlyRenderMessageLayout
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
@ -53,6 +55,8 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
}
}
(holder.view as? TimelineMessageLayoutRenderer).scOnlyRenderMessageLayout(attributes.informationData.messageLayout, this, holder)
}
override fun unbind(holder: Holder) {

View File

@ -16,6 +16,9 @@
package im.vector.app.features.home.room.detail.timeline.item
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
@ -24,15 +27,21 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.resources.LocaleProvider
import im.vector.app.core.resources.getLayoutDirectionFromCurrentLocale
import im.vector.app.core.ui.views.BubbleDependentView
import im.vector.app.core.ui.views.ReadReceiptsView
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
import im.vector.app.features.home.room.detail.timeline.view.setFlatRtl
@EpoxyModelClass(layout = R.layout.item_timeline_event_read_receipts)
abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(), ItemWithEvents, BubbleDependentView<ReadReceiptsItem.Holder> {
@EpoxyAttribute lateinit var eventId: String
@EpoxyAttribute lateinit var readReceipts: List<ReadReceiptData>
@EpoxyAttribute lateinit var messageLayout: TimelineMessageLayout
@EpoxyAttribute var shouldHideReadReceipts: Boolean = false
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: ClickListener
@ -46,6 +55,8 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
holder.readReceiptsView.onClick(clickListener)
holder.readReceiptsView.render(readReceipts, avatarRenderer)
(messageLayout as? TimelineMessageLayout.ScBubble)?.let { applyScBubbleStyle(it, holder) }
holder.readReceiptsView.isVisible = !shouldHideReadReceipts
}
@ -54,6 +65,46 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
super.unbind(holder)
}
override fun applyScBubbleStyle(messageLayout: TimelineMessageLayout.ScBubble, holder: Holder) {
val defaultDirection = LocaleProvider(holder.view.resources).getLayoutDirectionFromCurrentLocale()
val defaultRtl = defaultDirection == View.LAYOUT_DIRECTION_RTL
val reverseDirection = if (defaultRtl) View.LAYOUT_DIRECTION_LTR else View.LAYOUT_DIRECTION_RTL
/*
val receiptParent = holder.readReceiptsView.parent
if (receiptParent is LinearLayout) {
(holder.readReceiptsView.layoutParams as LinearLayout.LayoutParams).gravity = if (dualBubbles) Gravity.START else Gravity.END
(receiptParent.layoutParams as RelativeLayout.LayoutParams).removeRule(RelativeLayout.END_OF)
(receiptParent.layoutParams as RelativeLayout.LayoutParams).removeRule(RelativeLayout.ALIGN_PARENT_START)
if (dualBubbles) {
(receiptParent.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.ALIGN_PARENT_START, RelativeLayout.TRUE)
} else {
(receiptParent.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.END_OF, R.id.messageStartGuideline)
}
} else if (receiptParent is RelativeLayout) {
if (dualBubbles) {
(holder.readReceiptsView.layoutParams as RelativeLayout.LayoutParams).removeRule(RelativeLayout.ALIGN_PARENT_END)
} else {
(holder.readReceiptsView.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.ALIGN_PARENT_END)
}
} else if (receiptParent is FrameLayout) {
*/
if (messageLayout.singleSidedLayout) {
(holder.readReceiptsView.layoutParams as FrameLayout.LayoutParams).gravity = Gravity.END
} else {
(holder.readReceiptsView.layoutParams as FrameLayout.LayoutParams).gravity = Gravity.START
}
/*
} else {
Timber.e("Unsupported layout for read receipts parent: $receiptParent")
}
*/
// Also set rtl to have members fill from the natural side
setFlatRtl(holder.readReceiptsView, if (messageLayout.singleSidedLayout) defaultDirection else reverseDirection, defaultDirection)
}
class Holder : VectorEpoxyHolder() {
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
}

View File

@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.style
import android.os.Parcelable
import im.vector.app.R
import im.vector.app.features.home.room.detail.timeline.item.AnonymousReadReceipt
import kotlinx.parcelize.Parcelize
sealed interface TimelineMessageLayout : Parcelable {
@ -25,11 +26,13 @@ sealed interface TimelineMessageLayout : Parcelable {
val showAvatar: Boolean
val showDisplayName: Boolean
val showTimestamp: Boolean
val showE2eDecoration: Boolean
@Parcelize
data class Default(override val showAvatar: Boolean,
override val showDisplayName: Boolean,
override val showTimestamp: Boolean,
override val showE2eDecoration: Boolean,
// Keep defaultLayout generated on epoxy items
override val layoutRes: Int = 0) : TimelineMessageLayout
@ -38,6 +41,7 @@ sealed interface TimelineMessageLayout : Parcelable {
override val showAvatar: Boolean,
override val showDisplayName: Boolean,
override val showTimestamp: Boolean = true,
override val showE2eDecoration: Boolean = true,
val isIncoming: Boolean,
val isPseudoBubble: Boolean,
val cornersRadius: CornersRadius,
@ -60,22 +64,22 @@ sealed interface TimelineMessageLayout : Parcelable {
@Parcelize
data class ScBubble(
// SC-TODO adapt me
override val showAvatar: Boolean,
override val showDisplayName: Boolean,
override val showTimestamp: Boolean = true,
// SC-TODO?? Keep defaultLayout generated on epoxy items
override val layoutRes: Int = 0
/* SC-TODO
override val showE2eDecoration: Boolean = false,
val isIncoming: Boolean,
val reverseBubble: Boolean,
val singleSidedLayout: Boolean,
val isRealBubble: Boolean,
val isPseudoBubble: Boolean,
val isNotice: Boolean,
val timestampAsOverlay: Boolean,
override val layoutRes: Int = if (isIncoming) {
R.layout.item_timeline_event_bubble_incoming_base
R.layout.item_timeline_event_sc_bubble_incoming_base
} else {
R.layout.item_timeline_event_bubble_outgoing_base
R.layout.item_timeline_event_sc_bubble_outgoing_base
}
*/
) : TimelineMessageLayout
}

View File

@ -23,9 +23,11 @@ import im.vector.app.core.resources.LocaleProvider
import im.vector.app.core.resources.isRTL
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.themes.BubbleThemeUtils
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.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageNoticeContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@ -37,6 +39,7 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
private val layoutSettingsProvider: TimelineLayoutSettingsProvider,
private val localeProvider: LocaleProvider,
private val resources: Resources,
private val bubbleThemeUtils: BubbleThemeUtils,
private val vectorPreferences: VectorPreferences) {
companion object {
@ -96,8 +99,22 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
val messageLayout = when (layoutSettingsProvider.getLayoutSettings()) {
TimelineLayoutSettings.SC_BUBBLE -> {
// SC-TODO?
buildModernLayout(showInformation)
val messageContent = event.getLastMessageContent()
val isBubble = event.shouldBuildBubbleLayout()
val singleSidedLayout = bubbleThemeUtils.getBubbleStyle() == BubbleThemeUtils.BUBBLE_STYLE_START
val pseudoBubble = messageContent.isPseudoBubble()
return TimelineMessageLayout.ScBubble(
showAvatar = showInformation,
showDisplayName = showInformation,
showTimestamp = !singleSidedLayout || vectorPreferences.alwaysShowTimeStamps(),
isIncoming = !isSentByMe,
isNotice = messageContent is MessageNoticeContent,
reverseBubble = isSentByMe && !singleSidedLayout,
singleSidedLayout = singleSidedLayout,
isRealBubble = isBubble && !pseudoBubble,
isPseudoBubble = pseudoBubble,
timestampAsOverlay = messageContent.timestampAsOverlay()
)
}
TimelineLayoutSettings.MODERN -> {
buildModernLayout(showInformation)
@ -135,6 +152,35 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
return messageLayout
}
/**
* Just a dumb layout setting, so we get basic ScBubble settings in strictly-non-bubble classes as well
*/
fun createDummy(): TimelineMessageLayout {
return when (layoutSettingsProvider.getLayoutSettings()) {
TimelineLayoutSettings.SC_BUBBLE -> {
val singleSidedLayout = bubbleThemeUtils.getBubbleStyle() == BubbleThemeUtils.BUBBLE_STYLE_START
return TimelineMessageLayout.ScBubble(
showAvatar = false,
showDisplayName = false,
showTimestamp = true,
isIncoming = false,
isNotice = false,
reverseBubble = false,
singleSidedLayout = singleSidedLayout,
isRealBubble = false,
isPseudoBubble = false,
timestampAsOverlay = false
)
}
else -> TimelineMessageLayout.Default(
showAvatar = false,
showDisplayName = false,
showTimestamp = vectorPreferences.alwaysShowTimeStamps(),
showE2eDecoration = false
)
}
}
private fun MessageContent?.isPseudoBubble(): Boolean {
if (this == null) return false
if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline()
@ -156,19 +202,12 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
return false
}
private fun buildModernLayout(showInformation: Boolean): TimelineMessageLayout.Default {
private fun buildModernLayout(showInformation: Boolean, forScBubbles: Boolean = false): TimelineMessageLayout.Default {
return TimelineMessageLayout.Default(
showAvatar = showInformation,
showDisplayName = showInformation,
showTimestamp = showInformation || vectorPreferences.alwaysShowTimeStamps()
)
}
private fun buildScLayout(showInformation: Boolean): TimelineMessageLayout.ScBubble {
return TimelineMessageLayout.ScBubble(
showAvatar = showInformation,
showDisplayName = showInformation,
showTimestamp = true
showTimestamp = showInformation || vectorPreferences.alwaysShowTimeStamps(),
showE2eDecoration = !forScBubbles
)
}

View File

@ -3,10 +3,10 @@ package im.vector.app.features.home.room.detail.timeline.url
import android.view.View
import androidx.core.view.isVisible
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.media.ImageContentRenderer
interface AbstractPreviewUrlView: TimelineMessageLayoutRenderer {
interface AbstractPreviewUrlView {
var isVisible: Boolean
get() = (this as View).isVisible
set(value) { (this as View).isVisible = value }
@ -15,4 +15,7 @@ interface AbstractPreviewUrlView: TimelineMessageLayoutRenderer {
fun render(newState: PreviewUrlUiState,
imageContentRenderer: ImageContentRenderer,
force: Boolean = false)
// Like upstream TimelineMessageLayoutRenderer, not like downstream one, so don't inherit
fun renderMessageLayout(messageLayout: TimelineMessageLayout)
}

View File

@ -35,8 +35,10 @@ import com.google.android.material.shape.MaterialShapeDrawable
import im.vector.app.R
import im.vector.app.core.resources.LocaleProvider
import im.vector.app.core.resources.getLayoutDirectionFromCurrentLocale
import im.vector.app.core.ui.views.BubbleDependentView
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.databinding.ViewMessageBubbleBinding
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.style.shapeAppearanceModel
import im.vector.app.features.themes.ThemeUtils
@ -93,7 +95,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri
}
}
override fun renderMessageLayout(messageLayout: TimelineMessageLayout) {
override fun <H: BaseEventItem.BaseHolder>renderMessageLayout(messageLayout: TimelineMessageLayout, bubbleDependentView: BubbleDependentView<H>, holder: H) {
if (messageLayout !is TimelineMessageLayout.Bubble) {
Timber.v("Can't render messageLayout $messageLayout")
return

View File

@ -0,0 +1,609 @@
package im.vector.app.features.home.room.detail.timeline.view
import android.content.Context
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.content.withStyledAttributes
import androidx.core.view.children
import androidx.core.view.isVisible
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.resources.LocaleProvider
import im.vector.app.core.resources.getLayoutDirectionFromCurrentLocale
import im.vector.app.core.ui.views.BubbleDependentView
import im.vector.app.databinding.ViewMessageBubbleScBinding
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.AnonymousReadReceipt
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.themes.BubbleThemeUtils
import im.vector.app.features.themes.BubbleThemeUtils.Companion.BUBBLE_TIME_BOTTOM
import im.vector.app.features.themes.BubbleThemeUtils.Companion.BUBBLE_TIME_TOP
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.themes.guessTextWidth
import timber.log.Timber
import kotlin.math.ceil
import kotlin.math.max
class ScMessageBubbleWrapView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) :
RelativeLayout(context, attrs, defStyleAttr), TimelineMessageLayoutRenderer {
private var isIncoming: Boolean = false
private lateinit var views: ViewMessageBubbleScBinding
init {
inflate(context, R.layout.view_message_bubble_sc, this)
context.withStyledAttributes(attrs, R.styleable.MessageBubble) {
isIncoming = getBoolean(R.styleable.MessageBubble_incoming_style, false)
}
}
override fun onFinishInflate() {
super.onFinishInflate()
views = ViewMessageBubbleScBinding.bind(this)
// SC-TODO ... ?
}
fun <H : VectorEpoxyHolder> customBind(
bubbleDependentView: BubbleDependentView<H>,
holder: H,
attributes: AbsMessageItem.Attributes,
_avatarClickListener: ClickListener,
_memberNameClickListener: ClickListener): Boolean {
if (attributes.informationData.messageLayout !is TimelineMessageLayout.ScBubble) {
Timber.v("Can't render messageLayout ${attributes.informationData.messageLayout}")
return false
}
val contentInBubble = infoInBubbles(attributes.informationData.messageLayout)
val senderInBubble = senderNameInBubble(attributes.informationData.messageLayout)
val avatarImageView: ImageView?
var memberNameView: TextView?
var timeView: TextView?
val hiddenViews = ArrayList<View>()
val invisibleViews = ArrayList<View>()
val canHideAvatar = canHideAvatars(attributes)
val canHideSender = canHideSender(attributes)
// Select which views are visible, based on bubble style and other criteria
if (attributes.informationData.messageLayout.showDisplayName) {
if (senderInBubble) {
memberNameView = views.bubbleMessageMemberNameView
hiddenViews.add(views.messageMemberNameView)
} else {
memberNameView = views.messageMemberNameView
hiddenViews.add(views.bubbleMessageMemberNameView)
}
if (contentInBubble) {
timeView = views.bubbleMessageTimeView
hiddenViews.add(views.messageTimeView)
} else {
timeView = views.messageTimeView
hiddenViews.add(views.bubbleMessageTimeView)
}
} else if (attributes.informationData.messageLayout.showTimestamp) {
memberNameView = null
//hiddenViews.add(views.memberNameView) // this one get's some special hiding treatment below
hiddenViews.add(views.bubbleMessageMemberNameView)
if (contentInBubble) {
timeView = views.bubbleMessageTimeView
hiddenViews.add(views.messageTimeView)
hiddenViews.add(views.messageMemberNameView)
} else {
timeView = views.messageTimeView
hiddenViews.add(views.bubbleMessageTimeView)
// Set to INVISIBLE instead of adding to hiddenViews, which are set to GONE
// (upstream sets memberNameView.isInvisible = true here, which is effectively the same)
invisibleViews.add(views.messageMemberNameView)
}
} else {
memberNameView = null
hiddenViews.add(views.messageMemberNameView)
hiddenViews.add(views.bubbleMessageMemberNameView)
timeView = null
hiddenViews.add(views.messageTimeView)
hiddenViews.add(views.bubbleMessageTimeView)
}
if (timeView === views.bubbleMessageTimeView) {
// We have two possible bubble time view locations
// For code readability, we don't inline this setting in the above cases
if (getBubbleTimeLocation(attributes.informationData.messageLayout) == BubbleThemeUtils.BUBBLE_TIME_BOTTOM) {
timeView = views.bubbleFooterMessageTimeView
if (attributes.informationData.messageLayout.showDisplayName) {
if (canHideSender) {
// In the case of footer time, we can also hide the names without making it look awkward
if (memberNameView != null) {
hiddenViews.add(memberNameView)
memberNameView = null
}
hiddenViews.add(views.bubbleMessageTimeView)
} else if (!senderInBubble) {
// We don't need to reserve space here
hiddenViews.add(views.bubbleMessageTimeView)
} else {
// Don't completely remove, just hide, so our relative layout rules still work
invisibleViews.add(views.bubbleMessageTimeView)
}
} else {
// Do hide, or we accidentally reserve space
hiddenViews.add(views.bubbleMessageTimeView)
}
} else {
hiddenViews.add(views.bubbleFooterMessageTimeView)
}
}
// Dual-side bubbles: hide own avatar, and all in direct chats
if ((!attributes.informationData.messageLayout.showAvatar) ||
(contentInBubble && canHideAvatar)) {
avatarImageView = null
hiddenViews.add(views.messageAvatarImageView)
} else {
avatarImageView = views.messageAvatarImageView
}
// Views available in upstream Element
avatarImageView?.layoutParams = avatarImageView?.layoutParams?.apply {
height = attributes.avatarSize
width = attributes.avatarSize
}
avatarImageView?.visibility = View.VISIBLE
avatarImageView?.onClick(_avatarClickListener)
memberNameView?.visibility = View.VISIBLE
memberNameView?.onClick(_memberNameClickListener)
timeView?.visibility = View.VISIBLE
timeView?.text = attributes.informationData.time
memberNameView?.text = attributes.informationData.memberName
memberNameView?.setTextColor(attributes.getMemberNameColor())
if (avatarImageView != null) attributes.avatarRenderer.render(attributes.informationData.matrixItem, avatarImageView)
avatarImageView?.setOnLongClickListener(attributes.itemLongClickListener)
memberNameView?.setOnLongClickListener(attributes.itemLongClickListener)
// More extra views added by Schildi
if (senderInBubble) {
views.viewStubContainer.root.minimumWidth = getViewStubMinimumWidth(bubbleDependentView, holder, attributes, contentInBubble, canHideSender)
} else {
views.viewStubContainer.root.minimumWidth = 0
}
if (contentInBubble) {
views.bubbleFootView.visibility = View.VISIBLE
} else {
hiddenViews.add(views.bubbleFootView)
}
// Actually hide all unnecessary views
hiddenViews.forEach {
// Same as it.isVisible = false
it.visibility = View.GONE
}
invisibleViews.forEach {
// Same as it.isInvisible = true
it.visibility = View.INVISIBLE
}
// Render send state indicator
if (contentInBubble) {
// Bubbles have their own decoration in the anonymous read receipt (in the message footer)
views.messageSendStateImageView.isVisible = false
views.eventSendingIndicator.isVisible = false
} else {
views.messageSendStateImageView.render(attributes.informationData.sendStateDecoration)
views.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
}
return true
}
override fun <H : VectorEpoxyHolder> renderBaseMessageLayout(messageLayout: TimelineMessageLayout, bubbleDependentView: BubbleDependentView<H>, holder: H) {
if (messageLayout !is TimelineMessageLayout.ScBubble) {
Timber.v("Can't render messageLayout $messageLayout")
return
}
bubbleDependentView.applyScBubbleStyle(messageLayout, holder)
renderStubMessageLayout(messageLayout, views.viewStubContainer.root)
// Padding for views that align with the bubble (should be roughly the bubble tail width)
val bubbleStartAlignWidth = views.informationBottom.resources.getDimensionPixelSize(R.dimen.sc_bubble_tail_size)
if (messageLayout.reverseBubble) {
// Align reactions container to bubble
views.informationBottom.setPaddingRelative(
0,
0,
bubbleStartAlignWidth,
0
)
} else {
// Align reactions container to bubble
views.informationBottom.setPaddingRelative(
bubbleStartAlignWidth,
0,
0,
0
)
}
}
override fun <H : BaseEventItem.BaseHolder> renderMessageLayout(messageLayout: TimelineMessageLayout, bubbleDependentView: BubbleDependentView<H>, holder: H) {
if (messageLayout !is TimelineMessageLayout.ScBubble) {
Timber.v("Can't render messageLayout $messageLayout")
return
}
renderBaseMessageLayout(messageLayout, bubbleDependentView, holder)
val bubbleView = views.bubbleView
val informationData = bubbleDependentView.getInformationData()
val contentInBubble = infoInBubbles(messageLayout)
val defaultDirection = LocaleProvider(resources).getLayoutDirectionFromCurrentLocale()
val defaultRtl = defaultDirection == View.LAYOUT_DIRECTION_RTL
val reverseDirection = if (defaultRtl) View.LAYOUT_DIRECTION_LTR else View.LAYOUT_DIRECTION_RTL
// Notice formatting - also relevant if no actual bubbles are shown
bubbleView.alpha = if (messageLayout.isNotice) 0.65f else 1f
if (messageLayout.isRealBubble || messageLayout.isPseudoBubble) {
// Padding for bubble content: long for side with tail, short for other sides
val longPaddingDp: Int
val shortPaddingDp: Int
if (!messageLayout.isPseudoBubble) {
val bubbleRes = if (messageLayout.showAvatar) { // tail
if (messageLayout.reverseBubble) { // outgoing
R.drawable.msg_bubble_text_outgoing
} else { // incoming
R.drawable.msg_bubble_text_incoming
}
} else { // notail
if (messageLayout.reverseBubble) { // outgoing
R.drawable.msg_bubble_text_outgoing_notail
} else { // incoming
R.drawable.msg_bubble_text_incoming_notail
}
}
bubbleView.setBackgroundResource(bubbleRes)
longPaddingDp = bubbleView.resources.getDimensionPixelSize(R.dimen.sc_bubble_inner_padding_long_side)
shortPaddingDp = bubbleView.resources.getDimensionPixelSize(R.dimen.sc_bubble_inner_padding_short_side)
} else {
longPaddingDp = bubbleView.resources.getDimensionPixelSize(R.dimen.sc_bubble_tail_size)
shortPaddingDp = 0//if (attributes.informationData.showInformation && !hideSenderInformation()) { 8 } else { 0 }
}
if (messageLayout.reverseBubble != defaultRtl) {
// Use left/right instead of start/end: bubbleView is always LTR
(bubbleView.layoutParams as ViewGroup.MarginLayoutParams).leftMargin = bubbleDependentView.getScBubbleMargin(bubbleView.resources)
(bubbleView.layoutParams as ViewGroup.MarginLayoutParams).rightMargin = 0
} else {
(bubbleView.layoutParams as ViewGroup.MarginLayoutParams).leftMargin = 0
(bubbleView.layoutParams as ViewGroup.MarginLayoutParams).rightMargin = bubbleDependentView.getScBubbleMargin(bubbleView.resources)
}
if (messageLayout.reverseBubble != defaultRtl) {
// Use left/right instead of start/end: bubbleView is always LTR
bubbleView.setPadding(
shortPaddingDp,
shortPaddingDp,
longPaddingDp,
shortPaddingDp
)
} else {
bubbleView.setPadding(
longPaddingDp,
shortPaddingDp,
shortPaddingDp,
shortPaddingDp
)
}
if (contentInBubble) {
val anonymousReadReceipt = BubbleThemeUtils.getVisibleAnonymousReadReceipts(
informationData?.readReceiptAnonymous, !messageLayout.isIncoming)
when (anonymousReadReceipt) {
AnonymousReadReceipt.PROCESSING -> {
views.bubbleFooterReadReceipt.visibility = View.VISIBLE
views.bubbleFooterReadReceipt.setImageResource(R.drawable.ic_processing_msg)
}
else -> {
views.bubbleFooterReadReceipt.visibility = View.GONE
}
}
// We can't use end and start because of our weird layout RTL tricks
val alignEnd = if (defaultRtl) RelativeLayout.ALIGN_LEFT else RelativeLayout.ALIGN_RIGHT
val alignStart = if (defaultRtl) RelativeLayout.ALIGN_RIGHT else RelativeLayout.ALIGN_LEFT
val startOf = if (defaultRtl) RelativeLayout.RIGHT_OF else RelativeLayout.LEFT_OF
val endOf = if (defaultRtl) RelativeLayout.LEFT_OF else RelativeLayout.RIGHT_OF
val footerLayoutParams = views.bubbleFootView.layoutParams as RelativeLayout.LayoutParams
var footerMarginStartDp = views.bubbleFootView.resources.getDimensionPixelSize(R.dimen.sc_footer_margin_start)
var footerMarginEndDp = views.bubbleFootView.resources.getDimensionPixelSize(R.dimen.sc_footer_margin_end)
if (bubbleDependentView.allowFooterOverlay(holder, this)) {
footerLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, R.id.viewStubContainer)
footerLayoutParams.addRule(alignEnd, R.id.viewStubContainer)
footerLayoutParams.removeRule(alignStart)
footerLayoutParams.removeRule(RelativeLayout.BELOW)
footerLayoutParams.removeRule(endOf)
footerLayoutParams.removeRule(startOf)
if (bubbleDependentView.needsFooterReservation()) {
// Remove style used when not having reserved space
removeFooterOverlayStyle()
// Calculate required footer space
val footerMeasures = getFooterMeasures(informationData, anonymousReadReceipt)
val footerWidth = footerMeasures[0]
val footerHeight = footerMeasures[1]
bubbleDependentView.reserveFooterSpace(holder, footerWidth, footerHeight)
} else {
// We have no reserved space -> style it to ensure readability on arbitrary backgrounds
styleFooterOverlay()
}
} else {
when {
bubbleDependentView.allowFooterBelow(holder) -> {
footerLayoutParams.addRule(RelativeLayout.BELOW, R.id.viewStubContainer)
footerLayoutParams.addRule(alignEnd, R.id.viewStubContainer)
footerLayoutParams.removeRule(alignStart)
footerLayoutParams.removeRule(RelativeLayout.ALIGN_BOTTOM)
footerLayoutParams.removeRule(endOf)
footerLayoutParams.removeRule(startOf)
footerLayoutParams.removeRule(RelativeLayout.START_OF)
}
messageLayout.reverseBubble -> /* force footer on the left / at the start */ {
footerLayoutParams.addRule(RelativeLayout.START_OF, R.id.viewStubContainer)
footerLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, R.id.viewStubContainer)
footerLayoutParams.removeRule(alignEnd)
footerLayoutParams.removeRule(alignStart)
footerLayoutParams.removeRule(endOf)
footerLayoutParams.removeRule(startOf)
footerLayoutParams.removeRule(RelativeLayout.BELOW)
// Reverse margins
footerMarginStartDp = views.bubbleFootView.resources.getDimensionPixelSize(R.dimen.sc_footer_reverse_margin_start)
footerMarginEndDp = views.bubbleFootView.resources.getDimensionPixelSize(R.dimen.sc_footer_reverse_margin_end)
}
else -> /* footer on the right / at the end */ {
footerLayoutParams.addRule(endOf, R.id.viewStubContainer)
footerLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, R.id.viewStubContainer)
footerLayoutParams.removeRule(startOf)
footerLayoutParams.removeRule(alignEnd)
footerLayoutParams.removeRule(alignStart)
footerLayoutParams.removeRule(RelativeLayout.BELOW)
footerLayoutParams.removeRule(RelativeLayout.START_OF)
}
}
removeFooterOverlayStyle()
}
if (defaultRtl) {
footerLayoutParams.rightMargin = footerMarginStartDp
footerLayoutParams.leftMargin = footerMarginEndDp
views.bubbleMessageMemberNameView.gravity = Gravity.RIGHT
} else {
footerLayoutParams.leftMargin = footerMarginStartDp
footerLayoutParams.rightMargin = footerMarginEndDp
views.bubbleMessageMemberNameView.gravity = Gravity.LEFT
}
}
if (messageLayout.isPseudoBubble) {
// We need to align the non-bubble member name view to pseudo bubbles
if (messageLayout.reverseBubble) {
views.messageMemberNameView.setPaddingRelative(
shortPaddingDp,
0,
longPaddingDp,
0
)
} else {
views.messageMemberNameView.setPaddingRelative(
longPaddingDp,
0,
shortPaddingDp,
0
)
}
}
} else { // no bubbles
bubbleView.background = null
(bubbleView.layoutParams as ViewGroup.MarginLayoutParams).marginStart = 0
(bubbleView.layoutParams as ViewGroup.MarginLayoutParams).marginEnd = 0
/*
(bubbleView.layoutParams as RelativeLayout.LayoutParams).marginStart = 0
(bubbleView.layoutParams as RelativeLayout.LayoutParams).topMargin = 0
(bubbleView.layoutParams as RelativeLayout.LayoutParams).bottomMargin = 0
*/
bubbleView.setPadding(0, 0, 0, 0)
views.messageMemberNameView.setPadding(0, 0, 0, 0)
}
/*
holder.eventBaseView.layoutDirection = if (shouldRtl) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
setRtl(shouldRtl)
*/
(views.bubbleView.layoutParams as FrameLayout.LayoutParams).gravity = if (messageLayout.reverseBubble) Gravity.END else Gravity.START
//holder.informationBottom.layoutDirection = if (shouldRtl) View.LAYOUT_DIRECTION_RTL else View.LAYOUT_DIRECTION_LTR
setFlatRtl(views.reactionsContainer, if (messageLayout.reverseBubble) reverseDirection else defaultDirection, defaultDirection)
// Layout is broken if bubbleView is RTL (for some reason, Android uses left/end padding for right/start as well...)
setFlatRtl(views.bubbleView, View.LAYOUT_DIRECTION_LTR, defaultDirection)
}
private fun tintFooter(color: Int) {
val tintList = ColorStateList(arrayOf(intArrayOf(0)), intArrayOf(color))
views.bubbleFooterReadReceipt.imageTintList = tintList
views.bubbleFooterMessageTimeView.setTextColor(tintList)
}
private fun styleFooterOverlay() {
views.bubbleFootView.setBackgroundResource(R.drawable.timestamp_overlay)
tintFooter(ThemeUtils.getColor(views.bubbleFootView.context, R.attr.timestamp_overlay_fg))
val padding = views.bubbleFootView.resources.getDimensionPixelSize(R.dimen.sc_footer_overlay_padding)
views.bubbleFootView.setPaddingRelative(
padding,
padding,
// compensate from inner view padding on the other side
padding + views.bubbleFootView.resources.getDimensionPixelSize(R.dimen.sc_footer_padding_compensation),
padding
)
}
private fun removeFooterOverlayStyle() {
views.bubbleFootView.background = null
tintFooter(ThemeUtils.getColor(views.bubbleFootView.context, R.attr.vctr_content_secondary))
views.bubbleFootView.setPaddingRelative(
0,
views.bubbleFootView.resources.getDimensionPixelSize(R.dimen.sc_footer_noverlay_padding_top),
0,
views.bubbleFootView.resources.getDimensionPixelSize(R.dimen.sc_footer_noverlay_padding_bottom)
)
}
fun getFooterMeasures(informationData: MessageInformationData?): Array<Int> {
val anonymousReadReceipt = BubbleThemeUtils.getVisibleAnonymousReadReceipts(informationData?.readReceiptAnonymous, informationData?.sentByMe ?: false)
return getFooterMeasures(informationData, anonymousReadReceipt)
}
private fun getFooterMeasures(informationData: MessageInformationData?, anonymousReadReceipt: AnonymousReadReceipt): Array<Int> {
if (informationData == null) {
Timber.e("Calculating footer measures without information data")
}
val timeWidth: Int
val timeHeight: Int
if (informationData?.messageLayout is TimelineMessageLayout.ScBubble &&
getBubbleTimeLocation(informationData.messageLayout as TimelineMessageLayout.ScBubble) == BubbleThemeUtils.BUBBLE_TIME_BOTTOM) {
// Guess text width for name and time
timeWidth = ceil(guessTextWidth(views.bubbleFooterMessageTimeView, informationData.time.toString())).toInt() +
views.bubbleFooterMessageTimeView.paddingLeft +
views.bubbleFooterMessageTimeView.paddingRight
timeHeight = ceil(views.bubbleFooterMessageTimeView.textSize).toInt() +
views.bubbleFooterMessageTimeView.paddingTop +
views.bubbleFooterMessageTimeView.paddingBottom
} else {
timeWidth = 0
timeHeight = 0
}
val readReceiptWidth: Int
val readReceiptHeight: Int
if (anonymousReadReceipt == AnonymousReadReceipt.NONE) {
readReceiptWidth = 0
readReceiptHeight = 0
} else {
readReceiptWidth = views.bubbleFooterReadReceipt.maxWidth +
views.bubbleFooterReadReceipt.paddingLeft +
views.bubbleFooterReadReceipt.paddingRight
readReceiptHeight = views.bubbleFooterReadReceipt.maxHeight +
views.bubbleFooterReadReceipt.paddingTop +
views.bubbleFooterReadReceipt.paddingBottom
}
var footerWidth = timeWidth + readReceiptWidth
var footerHeight = max(timeHeight, readReceiptHeight)
// Reserve extra padding, if we do have actual content
if (footerWidth > 0) {
footerWidth += views.bubbleFootView.paddingLeft + views.bubbleFootView.paddingRight
}
if (footerHeight > 0) {
footerHeight += views.bubbleFootView.paddingTop + views.bubbleFootView.paddingBottom
}
return arrayOf(footerWidth, footerHeight)
}
fun <H : VectorEpoxyHolder> getViewStubMinimumWidth(bubbleDependentView: BubbleDependentView<H>,
holder: H,
attributes: AbsMessageItem.Attributes,
contentInBubble: Boolean,
canHideSender: Boolean): Int {
val messageLayout = attributes.informationData.messageLayout as? TimelineMessageLayout.ScBubble ?: return 0
val memberName = attributes.informationData.memberName.toString()
val time = attributes.informationData.time.toString()
val result = if (contentInBubble) {
if (getBubbleTimeLocation(messageLayout) == BUBBLE_TIME_BOTTOM) {
if (attributes.informationData.messageLayout.showDisplayName && canHideSender) {
// Since timeView automatically gets enough space, either within or outside the viewStub, we just need to ensure the member name view has enough space
// Somehow not enough without extra space...
ceil(guessTextWidth(views.bubbleMessageMemberNameView, "$memberName ")).toInt()
} else {
// wrap_content works!
0
}
} else if (attributes.informationData.messageLayout.showTimestamp) {
// Guess text width for name and time next to each other
val text = if (attributes.informationData.messageLayout.showDisplayName) {
"$memberName $time"
} else {
time
}
val textSize = if (attributes.informationData.messageLayout.showDisplayName) {
max(views.bubbleMessageMemberNameView.textSize, views.bubbleMessageTimeView.textSize)
} else {
views.bubbleMessageTimeView.textSize
}
ceil(guessTextWidth(textSize, text)).toInt()
} else {
// Not showing any header, use wrap_content of content only
0
}
} else {
0
}
return max(result, bubbleDependentView.getViewStubMinimumWidth(holder))
}
}
fun canHideAvatars(attributes: AbsMessageItem.Attributes): Boolean {
return attributes.informationData.sentByMe || attributes.informationData.isDirect
}
fun canHideSender(attributes: AbsMessageItem.Attributes): Boolean {
return attributes.informationData.sentByMe ||
(attributes.informationData.isDirect && attributes.informationData.senderId == attributes.informationData.dmChatPartnerId)
}
fun infoInBubbles(messageLayout: TimelineMessageLayout.ScBubble): Boolean {
return (!messageLayout.singleSidedLayout) &&
(messageLayout.isRealBubble || messageLayout.isPseudoBubble)
}
fun senderNameInBubble(messageLayout: TimelineMessageLayout.ScBubble): Boolean {
return infoInBubbles(messageLayout) && !messageLayout.isPseudoBubble
}
fun getBubbleTimeLocation(messageLayout: TimelineMessageLayout.ScBubble): String {
return if (messageLayout.singleSidedLayout) BUBBLE_TIME_TOP else BUBBLE_TIME_BOTTOM
}
fun setFlatRtl(layout: ViewGroup, direction: Int, childDirection: Int, depth: Int = 1) {
layout.layoutDirection = direction
for (child in layout.children) {
if (depth > 1 && child is ViewGroup) {
setFlatRtl(child, direction, childDirection, depth-1)
} else {
child.layoutDirection = childDirection
}
}
}
// Static to use from classes that use simplified/non-sc layouts, e.g. item_timeline_event_base_noinfo
fun renderStubMessageLayout(messageLayout: TimelineMessageLayout, viewStubContainer: FrameLayout) {
if (messageLayout !is TimelineMessageLayout.ScBubble) {
return
}
// Remove Element's TimelineContentStubContainerParams paddings, we don't want these
viewStubContainer.setPadding(0, 0, 0, 0)
}

View File

@ -16,8 +16,38 @@
package im.vector.app.features.home.room.detail.timeline.view
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.ui.views.BubbleDependentView
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
interface TimelineMessageLayoutRenderer {
fun renderMessageLayout(messageLayout: TimelineMessageLayout)
fun <H: BaseEventItem.BaseHolder>renderMessageLayout(messageLayout: TimelineMessageLayout,
bubbleDependentView: BubbleDependentView<H>,
holder: H)
// Variant to use from classes that do not use BaseEventItem.BaseHolder, and don't need the heavy bubble stuff
fun <H: VectorEpoxyHolder>renderBaseMessageLayout(messageLayout: TimelineMessageLayout,
bubbleDependentView: BubbleDependentView<H>,
holder: H) {}
}
// Only render message layout for SC layouts - even if parent is not an ScBubble
fun <H: BaseEventItem.BaseHolder>TimelineMessageLayoutRenderer?.scOnlyRenderMessageLayout(messageLayout: TimelineMessageLayout,
bubbleDependentView: BubbleDependentView<H>,
holder: H) {
if (messageLayout is TimelineMessageLayout.ScBubble) {
scRenderMessageLayout(messageLayout, bubbleDependentView, holder)
}
}
// Also render stub in case parent is no ScBubble
fun <H: BaseEventItem.BaseHolder>TimelineMessageLayoutRenderer?.scRenderMessageLayout(messageLayout: TimelineMessageLayout,
bubbleDependentView: BubbleDependentView<H>,
holder: H) {
if (this == null) {
renderStubMessageLayout(messageLayout, holder.viewStubContainer)
} else {
renderMessageLayout(messageLayout, bubbleDependentView, holder)
}
}

View File

@ -5,6 +5,8 @@ import android.graphics.Paint
import android.widget.TextView
import androidx.preference.PreferenceManager
import im.vector.app.features.home.room.detail.timeline.item.AnonymousReadReceipt
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import timber.log.Timber
import javax.inject.Inject
@ -22,6 +24,29 @@ class BubbleThemeUtils @Inject constructor(private val context: Context) {
const val BUBBLE_STYLE_BOTH = "both"
const val BUBBLE_TIME_TOP = "top"
const val BUBBLE_TIME_BOTTOM = "bottom"
fun getVisibleAnonymousReadReceipts(readReceipt: AnonymousReadReceipt?, sentByMe: Boolean): AnonymousReadReceipt {
readReceipt ?: return AnonymousReadReceipt.NONE
// TODO
return if (sentByMe && (/*TODO setting?*/ true || readReceipt == AnonymousReadReceipt.PROCESSING)) {
readReceipt
} else {
AnonymousReadReceipt.NONE
}
}
fun anonymousReadReceiptForEvent(event: TimelineEvent): AnonymousReadReceipt {
return if (event.root.sendState == SendState.SYNCED || event.root.sendState == SendState.SENT) {
/*if (event.readByOther) {
AnonymousReadReceipt.READ
} else {
AnonymousReadReceipt.SENT
}*/
AnonymousReadReceipt.NONE
} else {
AnonymousReadReceipt.PROCESSING
}
}
}
// Special case of BUBBLE_STYLE_BOTH, to allow non-bubble items align to the sender either way
@ -47,15 +72,6 @@ class BubbleThemeUtils @Inject constructor(private val context: Context) {
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(BUBBLE_STYLE_KEY, value).apply()
}
fun getVisibleAnonymousReadReceipts(readReceipt: AnonymousReadReceipt, sentByMe: Boolean): AnonymousReadReceipt {
// TODO
return if (sentByMe && (/*TODO setting*/ true || readReceipt == AnonymousReadReceipt.PROCESSING)) {
readReceipt
} else {
AnonymousReadReceipt.NONE
}
}
/* SC-TODO
fun drawsActualBubbles(bubbleStyle: String): Boolean {
return bubbleStyle == BUBBLE_STYLE_START || bubbleStyle == BUBBLE_STYLE_BOTH
@ -66,20 +82,6 @@ class BubbleThemeUtils @Inject constructor(private val context: Context) {
}
*/
fun guessTextWidth(view: TextView): Float {
return guessTextWidth(view, view.text)
}
fun guessTextWidth(view: TextView, text: CharSequence): Float {
return guessTextWidth(view.textSize, text);
}
fun guessTextWidth(textSize: Float, text: CharSequence): Float {
val paint = Paint()
paint.textSize = textSize
return paint.measureText(text.toString())
}
fun forceAlwaysShowTimestamps(bubbleStyle: String): Boolean {
return isBubbleTimeLocationSettingAllowed(bubbleStyle)
}
@ -96,3 +98,17 @@ class BubbleThemeUtils @Inject constructor(private val context: Context) {
return isBubbleTimeLocationSettingAllowed(getBubbleStyle())
}
}
fun guessTextWidth(view: TextView): Float {
return guessTextWidth(view, view.text)
}
fun guessTextWidth(view: TextView, text: CharSequence): Float {
return guessTextWidth(view.textSize, text);
}
fun guessTextWidth(textSize: Float, text: CharSequence): Float {
val paint = Paint()
paint.textSize = textSize
return paint.measureText(text.toString())
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
app:incoming_style="true" />

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
app:incoming_style="false" />

View File

@ -1,13 +1,12 @@
<?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/eventBaseView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
android:background="?attr/selectableItemBackground"
tools:viewBindingIgnore="true">
tools:parentTag="android.widget.RelativeLayout">
<im.vector.app.core.platform.CheckableView
android:id="@+id/messageSelectedBackground"
@ -116,7 +115,7 @@
android:textColor="?vctr_content_primary"
android:textSize="15sp"
android:textStyle="bold"
tools:text="@sample/matrix.json/data/displayName" />
tools:text="@sample/users.json/data/displayName" />
<TextView
android:id="@+id/bubbleMessageTimeView"
@ -136,7 +135,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/bubbleMessageTimeView"
android:layout_margin="0dp"
android:layout_marginStart="0dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="0dp"
android:layout_marginBottom="0dp"
android:addStatesFromChildren="true" />
<!--
@ -278,4 +280,4 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>
</merge>