[bubble merge] add back SC bubbles
Change-Id: Ia884fbc5d83aad8cb79d674ec7c411bff608e4e5
This commit is contained in:
parent
1024761526
commit
ef4c35499c
|
@ -1,11 +1,26 @@
|
||||||
package im.vector.app.core.ui.views
|
package im.vector.app.core.ui.views
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.res.Resources
|
||||||
import android.view.ViewGroup
|
import im.vector.app.R
|
||||||
import androidx.core.view.children
|
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||||
import im.vector.app.features.themes.BubbleThemeUtils
|
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 {
|
fun messageBubbleAllowed(context: Context): Boolean {
|
||||||
|
|
|
@ -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.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.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 im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
|
@ -46,6 +47,7 @@ import javax.inject.Inject
|
||||||
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
||||||
private val avatarRenderer: AvatarRenderer,
|
private val avatarRenderer: AvatarRenderer,
|
||||||
private val avatarSizeProvider: AvatarSizeProvider,
|
private val avatarSizeProvider: AvatarSizeProvider,
|
||||||
|
private val messageLayoutFactory: TimelineMessageLayoutFactory,
|
||||||
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||||
|
|
||||||
private val collapsedEventIds = linkedSetOf<Long>()
|
private val collapsedEventIds = linkedSetOf<Long>()
|
||||||
|
@ -129,7 +131,8 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
||||||
onCollapsedStateChanged = {
|
onCollapsedStateChanged = {
|
||||||
mergeItemCollapseStates[event.localId] = it
|
mergeItemCollapseStates[event.localId] = it
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
}
|
},
|
||||||
|
messageLayout = messageLayoutFactory.createDummy()
|
||||||
)
|
)
|
||||||
MergedMembershipEventsItem_()
|
MergedMembershipEventsItem_()
|
||||||
.id(mergeId)
|
.id(mergeId)
|
||||||
|
@ -206,6 +209,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
||||||
mergeItemCollapseStates[event.localId] = it
|
mergeItemCollapseStates[event.localId] = it
|
||||||
requestModelBuild()
|
requestModelBuild()
|
||||||
},
|
},
|
||||||
|
messageLayout = messageLayoutFactory.createDummy(),
|
||||||
hasEncryptionEvent = hasEncryption,
|
hasEncryptionEvent = hasEncryption,
|
||||||
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
|
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
|
||||||
callback = callback,
|
callback = callback,
|
||||||
|
|
|
@ -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.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.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 org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||||
import javax.inject.Inject
|
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(
|
fun create(
|
||||||
eventId: String,
|
eventId: String,
|
||||||
|
@ -44,6 +46,7 @@ class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: Av
|
||||||
.id("read_receipts_$eventId")
|
.id("read_receipts_$eventId")
|
||||||
.eventId(eventId)
|
.eventId(eventId)
|
||||||
.readReceipts(readReceiptsData)
|
.readReceipts(readReceiptsData)
|
||||||
|
.messageLayout(messageLayoutFactory.createDummy())
|
||||||
.avatarRenderer(avatarRenderer)
|
.avatarRenderer(avatarRenderer)
|
||||||
.shouldHideReadReceipts(isFromThreadTimeLine)
|
.shouldHideReadReceipts(isFromThreadTimeLine)
|
||||||
.clickListener {
|
.clickListener {
|
||||||
|
|
|
@ -161,16 +161,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||||
ReferencesInfoData(verificationState)
|
ReferencesInfoData(verificationState)
|
||||||
},
|
},
|
||||||
sentByMe = isSentByMe,
|
sentByMe = isSentByMe,
|
||||||
readReceiptAnonymous = if (event.root.sendState == SendState.SYNCED || event.root.sendState == SendState.SENT) {
|
readReceiptAnonymous = BubbleThemeUtils.anonymousReadReceiptForEvent(event),
|
||||||
/*if (event.readByOther) {
|
|
||||||
AnonymousReadReceipt.READ
|
|
||||||
} else {
|
|
||||||
AnonymousReadReceipt.SENT
|
|
||||||
}*/
|
|
||||||
AnonymousReadReceipt.NONE
|
|
||||||
} else {
|
|
||||||
AnonymousReadReceipt.PROCESSING
|
|
||||||
},
|
|
||||||
senderPowerLevel = senderPowerLevel,
|
senderPowerLevel = senderPowerLevel,
|
||||||
isDirect = isEffectivelyDirect,
|
isDirect = isEffectivelyDirect,
|
||||||
isPublic = roomSummary?.isPublic ?: false,
|
isPublic = roomSummary?.isPublic ?: false,
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.item
|
package im.vector.app.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
@ -25,13 +26,19 @@ import androidx.core.view.isVisible
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.epoxy.ClickListener
|
import im.vector.app.core.epoxy.ClickListener
|
||||||
import im.vector.app.core.epoxy.onClick
|
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.core.ui.views.ShieldImageView
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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.MessageColorProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
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.TimelineMessageLayoutRenderer
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.view.scRenderMessageLayout
|
||||||
import im.vector.app.features.reactions.widget.ReactionButton
|
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 org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base timeline item with reactions and read receipts.
|
* Base timeline item with reactions and read receipts.
|
||||||
|
@ -85,23 +92,22 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
|
||||||
holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
|
holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SchildiChat: moved to setBubbleLayout() (called from super.bind()) - so we can do this bubble-style-specific
|
if (baseAttributes.informationData.messageLayout.showE2eDecoration) {
|
||||||
/*
|
when (baseAttributes.informationData.e2eDecoration) {
|
||||||
when (baseAttributes.informationData.e2eDecoration) {
|
E2EDecoration.NONE -> {
|
||||||
E2EDecoration.NONE -> {
|
holder.e2EDecorationView.render(null)
|
||||||
holder.e2EDecorationView.render(null)
|
}
|
||||||
}
|
E2EDecoration.WARN_IN_CLEAR,
|
||||||
E2EDecoration.WARN_IN_CLEAR,
|
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
|
||||||
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
|
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
|
||||||
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
|
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
|
||||||
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
holder.view.onClick(baseAttributes.itemClickListener)
|
holder.view.onClick(baseAttributes.itemClickListener)
|
||||||
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
|
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) {
|
override fun unbind(holder: H) {
|
||||||
|
@ -116,6 +122,25 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
|
||||||
failureIndicator?.isVisible = baseAttributes.informationData.sendState.hasFailed()
|
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) {
|
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
|
||||||
val reactionsContainer by bind<ViewGroup>(R.id.reactionsContainer)
|
val reactionsContainer by bind<ViewGroup>(R.id.reactionsContainer)
|
||||||
val informationBottom by bind<ViewGroup>(R.id.informationBottom)
|
val informationBottom by bind<ViewGroup>(R.id.informationBottom)
|
||||||
|
|
|
@ -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.MessageColorProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
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.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.session.threads.ThreadDetails
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
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
|
* 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) {
|
override fun bind(holder: H) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
if (attributes.informationData.messageLayout.showAvatar) {
|
if ((holder.view as? ScMessageBubbleWrapView)?.customBind(this, holder, attributes, _avatarClickListener, _memberNameClickListener) != true) {
|
||||||
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
|
if (attributes.informationData.messageLayout.showAvatar) {
|
||||||
height = attributes.avatarSize
|
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
|
||||||
width = attributes.avatarSize
|
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)
|
if (attributes.informationData.messageLayout.showDisplayName) {
|
||||||
holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener)
|
holder.memberNameView.isVisible = true
|
||||||
holder.avatarImageView.isVisible = true
|
holder.memberNameView.text = attributes.informationData.memberName
|
||||||
holder.avatarImageView.onClick(_avatarClickListener)
|
holder.memberNameView.setTextColor(attributes.getMemberNameColor())
|
||||||
} else {
|
holder.memberNameView.onClick(_memberNameClickListener)
|
||||||
holder.avatarImageView.setOnClickListener(null)
|
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
holder.avatarImageView.setOnLongClickListener(null)
|
} else {
|
||||||
holder.avatarImageView.isVisible = false
|
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
|
// Threads
|
||||||
if (attributes.areThreadMessagesEnabled) {
|
if (attributes.areThreadMessagesEnabled) {
|
||||||
|
@ -151,14 +160,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||||
super.unbind(holder)
|
super.unbind(holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Attributes.getMemberNameColor() = messageColorProvider.getMemberNameTextColor(
|
override fun getInformationData(): MessageInformationData? = attributes.informationData
|
||||||
informationData.matrixItem,
|
|
||||||
MatrixItemColorProvider.UserInRoomInformation(
|
|
||||||
attributes.informationData.isDirect,
|
|
||||||
attributes.informationData.isPublic,
|
|
||||||
attributes.informationData.senderPowerLevel
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) {
|
abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) {
|
||||||
|
|
||||||
|
@ -216,10 +218,20 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMemberNameColor() = messageColorProvider.getMemberNameTextColor(
|
||||||
|
informationData.matrixItem,
|
||||||
|
MatrixItemColorProvider.UserInRoomInformation(
|
||||||
|
informationData.isDirect,
|
||||||
|
informationData.isPublic,
|
||||||
|
informationData.senderPowerLevel
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun ignoreMessageGuideline(context: Context): Boolean {
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.item
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewStub
|
import android.view.ViewStub
|
||||||
|
import android.widget.FrameLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.annotation.CallSuper
|
import androidx.annotation.CallSuper
|
||||||
import androidx.annotation.IdRes
|
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.epoxy.VectorEpoxyModel
|
||||||
import im.vector.app.core.platform.CheckableView
|
import im.vector.app.core.platform.CheckableView
|
||||||
import im.vector.app.core.ui.views.BubbleDependentView
|
import im.vector.app.core.ui.views.BubbleDependentView
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Children must override getViewType()
|
* 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 leftGuideline by bind<View>(R.id.messageStartGuideline)
|
||||||
val contentContainer by bind<View>(R.id.viewStubContainer)
|
val contentContainer by bind<View>(R.id.viewStubContainer)
|
||||||
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
||||||
|
val viewStubContainer by bind<FrameLayout>(R.id.viewStubContainer)
|
||||||
|
|
||||||
override fun bindView(itemView: View) {
|
override fun bindView(itemView: View) {
|
||||||
super.bindView(itemView)
|
super.bindView(itemView)
|
||||||
|
|
|
@ -21,6 +21,9 @@ import android.widget.TextView
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>() {
|
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.separatorView.visibility = View.VISIBLE
|
||||||
holder.expandView.setText(R.string.merged_events_collapse)
|
holder.expandView.setText(R.string.merged_events_collapse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(holder.view as? TimelineMessageLayoutRenderer).scOnlyRenderMessageLayout(attributes.messageLayout, this, holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected val distinctMergeData by lazy {
|
protected val distinctMergeData by lazy {
|
||||||
|
@ -69,6 +74,7 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
|
||||||
val mergeData: List<Data>
|
val mergeData: List<Data>
|
||||||
val avatarRenderer: AvatarRenderer
|
val avatarRenderer: AvatarRenderer
|
||||||
val onCollapsedStateChanged: (Boolean) -> Unit
|
val onCollapsedStateChanged: (Boolean) -> Unit
|
||||||
|
val messageLayout: TimelineMessageLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
|
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
|
||||||
|
|
|
@ -23,6 +23,8 @@ import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||||
abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
|
abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
|
||||||
|
@ -35,6 +37,8 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
|
||||||
holder.messageTextView.text = attributes.text
|
holder.messageTextView.text = attributes.text
|
||||||
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
|
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
|
||||||
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
|
|
||||||
|
(holder.view as? TimelineMessageLayoutRenderer).scOnlyRenderMessageLayout(attributes.informationData.messageLayout, this, holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
override fun unbind(holder: Holder) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||||
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
|
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
|
||||||
|
@ -69,6 +70,7 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
|
||||||
override val isCollapsed: Boolean,
|
override val isCollapsed: Boolean,
|
||||||
override val mergeData: List<Data>,
|
override val mergeData: List<Data>,
|
||||||
override val avatarRenderer: AvatarRenderer,
|
override val avatarRenderer: AvatarRenderer,
|
||||||
override val onCollapsedStateChanged: (Boolean) -> Unit
|
override val onCollapsedStateChanged: (Boolean) -> Unit,
|
||||||
|
override val messageLayout: TimelineMessageLayout
|
||||||
) : BasedMergedItem.Attributes
|
) : BasedMergedItem.Attributes
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ import im.vector.app.core.utils.tappableMatchingText
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
import im.vector.app.features.home.AvatarRenderer
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
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.TimelineEventController
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
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 mergeData: List<Data>,
|
||||||
override val avatarRenderer: AvatarRenderer,
|
override val avatarRenderer: AvatarRenderer,
|
||||||
override val onCollapsedStateChanged: (Boolean) -> Unit,
|
override val onCollapsedStateChanged: (Boolean) -> Unit,
|
||||||
|
override val messageLayout: TimelineMessageLayout,
|
||||||
val callback: TimelineEventController.Callback? = null,
|
val callback: TimelineEventController.Callback? = null,
|
||||||
val currentUserId: String,
|
val currentUserId: String,
|
||||||
val hasEncryptionEvent: Boolean,
|
val hasEncryptionEvent: Boolean,
|
||||||
|
|
|
@ -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.helper.ContentUploadStateTrackerBinder
|
||||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||||
|
@ -103,6 +106,18 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||||
contentDownloadStateTrackerBinder.unbind(mxcUrl)
|
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
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||||
|
|
|
@ -16,9 +16,11 @@
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.item
|
package im.vector.app.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
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.helper.ContentUploadStateTrackerBinder
|
||||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
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.style.granularRoundedCorners
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
|
||||||
import im.vector.app.features.media.ImageContentRenderer
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
import org.matrix.android.sdk.api.util.MimeTypes
|
import org.matrix.android.sdk.api.util.MimeTypes
|
||||||
|
|
||||||
|
@ -57,7 +60,6 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
|
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
|
||||||
|
|
||||||
// SC-TODO
|
|
||||||
var lastAllowedFooterOverlay: Boolean = true
|
var lastAllowedFooterOverlay: Boolean = true
|
||||||
var lastShowFooterBellow: Boolean = true
|
var lastShowFooterBellow: Boolean = true
|
||||||
var forceAllowFooterOverlay: Boolean? = null
|
var forceAllowFooterOverlay: Boolean? = null
|
||||||
|
@ -67,18 +69,19 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||||
forceAllowFooterOverlay = null
|
forceAllowFooterOverlay = null
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
|
|
||||||
|
val bubbleWrapView = (holder.view as? ScMessageBubbleWrapView)
|
||||||
|
val host = this
|
||||||
val onImageSizeListener = object: ImageContentRenderer.OnImageSizeListener {
|
val onImageSizeListener = object: ImageContentRenderer.OnImageSizeListener {
|
||||||
override fun onImageSizeUpdated(width: Int, height: Int) {
|
override fun onImageSizeUpdated(width: Int, height: Int) {
|
||||||
|
bubbleWrapView ?: return
|
||||||
// Image size change -> different footer space situation possible
|
// Image size change -> different footer space situation possible
|
||||||
/* SC-TODO
|
val footerMeasures = bubbleWrapView.getFooterMeasures(attributes.informationData)
|
||||||
val footerMeasures = getFooterMeasures(holder)
|
|
||||||
forceAllowFooterOverlay = shouldAllowFooterOverlay(footerMeasures, width, height)
|
forceAllowFooterOverlay = shouldAllowFooterOverlay(footerMeasures, width, height)
|
||||||
val newShowFooterBellow = shouldShowFooterBellow(footerMeasures, width, height)
|
val newShowFooterBellow = shouldShowFooterBellow(footerMeasures, width, height)
|
||||||
if (lastAllowedFooterOverlay != forceAllowFooterOverlay || newShowFooterBellow != lastShowFooterBellow) {
|
if (lastAllowedFooterOverlay != forceAllowFooterOverlay || newShowFooterBellow != lastShowFooterBellow) {
|
||||||
showFooterBellow = newShowFooterBellow
|
showFooterBellow = newShowFooterBellow
|
||||||
updateMessageBubble(holder.imageView.context, holder)
|
bubbleWrapView.renderMessageLayout(attributes.informationData.messageLayout, host, holder)
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val animate = mediaData.mimeType == MimeTypes.Gif
|
val animate = mediaData.mimeType == MimeTypes.Gif
|
||||||
|
@ -87,11 +90,10 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||||
|
|
||||||
val messageLayout = baseAttributes.informationData.messageLayout
|
val messageLayout = baseAttributes.informationData.messageLayout
|
||||||
val dimensionConverter = DimensionConverter(holder.view.resources)
|
val dimensionConverter = DimensionConverter(holder.view.resources)
|
||||||
// SC-TODO handle SC bubbles
|
val imageCornerTransformation = when (messageLayout) {
|
||||||
val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
|
is TimelineMessageLayout.ScBubble -> RoundedCorners(dimensionConverter.dpToPx(3))
|
||||||
messageLayout.cornersRadius.granularRoundedCorners()
|
is TimelineMessageLayout.Bubble -> messageLayout.cornersRadius.granularRoundedCorners()
|
||||||
} else {
|
else -> RoundedCorners(dimensionConverter.dpToPx(8))
|
||||||
RoundedCorners(dimensionConverter.dpToPx(8))
|
|
||||||
}
|
}
|
||||||
imageContentRenderer.render(mediaData, effectiveMode, holder.imageView, imageCornerTransformation, onImageSizeListener)
|
imageContentRenderer.render(mediaData, effectiveMode, holder.imageView, imageCornerTransformation, onImageSizeListener)
|
||||||
if (!attributes.informationData.sendState.hasFailed()) {
|
if (!attributes.informationData.sendState.hasFailed()) {
|
||||||
|
@ -122,7 +124,6 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
||||||
|
|
||||||
override fun getViewStubId() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
// SC-TODO
|
|
||||||
private fun shouldAllowFooterOverlay(footerMeasures: Array<Int>, imageWidth: Int, imageHeight: Int): Boolean {
|
private fun shouldAllowFooterOverlay(footerMeasures: Array<Int>, imageWidth: Int, imageHeight: Int): Boolean {
|
||||||
val footerWidth = footerMeasures[0]
|
val footerWidth = footerMeasures[0]
|
||||||
val footerHeight = footerMeasures[1]
|
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)
|
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 {
|
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).
|
// 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
|
// 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
|
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) {
|
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||||
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
|
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
|
||||||
|
|
|
@ -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.PreviewUrlUiState
|
||||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView
|
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.url.PreviewUrlViewSc
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
|
||||||
import im.vector.app.features.media.ImageContentRenderer
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
||||||
import io.noties.markwon.MarkwonPlugin
|
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 {
|
companion object {
|
||||||
private const val STUB_ID = R.id.messageContentTextStub
|
private const val STUB_ID = R.id.messageContentTextStub
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@ import im.vector.app.core.epoxy.onClick
|
||||||
import im.vector.app.core.ui.views.ShieldImageView
|
import im.vector.app.core.ui.views.ShieldImageView
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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.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 im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
||||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||||
|
|
||||||
|
@ -53,6 +55,8 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
||||||
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
|
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(holder.view as? TimelineMessageLayoutRenderer).scOnlyRenderMessageLayout(attributes.informationData.messageLayout, this, holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
override fun unbind(holder: Holder) {
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.item
|
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 androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
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.ClickListener
|
||||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||||
import im.vector.app.core.epoxy.onClick
|
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.BubbleDependentView
|
||||||
import im.vector.app.core.ui.views.ReadReceiptsView
|
import im.vector.app.core.ui.views.ReadReceiptsView
|
||||||
import im.vector.app.features.home.AvatarRenderer
|
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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_read_receipts)
|
||||||
abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(), ItemWithEvents, BubbleDependentView<ReadReceiptsItem.Holder> {
|
abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(), ItemWithEvents, BubbleDependentView<ReadReceiptsItem.Holder> {
|
||||||
|
|
||||||
@EpoxyAttribute lateinit var eventId: String
|
@EpoxyAttribute lateinit var eventId: String
|
||||||
@EpoxyAttribute lateinit var readReceipts: List<ReadReceiptData>
|
@EpoxyAttribute lateinit var readReceipts: List<ReadReceiptData>
|
||||||
|
@EpoxyAttribute lateinit var messageLayout: TimelineMessageLayout
|
||||||
@EpoxyAttribute var shouldHideReadReceipts: Boolean = false
|
@EpoxyAttribute var shouldHideReadReceipts: Boolean = false
|
||||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
|
||||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: ClickListener
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: ClickListener
|
||||||
|
@ -46,6 +55,8 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
|
||||||
holder.readReceiptsView.onClick(clickListener)
|
holder.readReceiptsView.onClick(clickListener)
|
||||||
holder.readReceiptsView.render(readReceipts, avatarRenderer)
|
holder.readReceiptsView.render(readReceipts, avatarRenderer)
|
||||||
|
|
||||||
|
(messageLayout as? TimelineMessageLayout.ScBubble)?.let { applyScBubbleStyle(it, holder) }
|
||||||
|
|
||||||
holder.readReceiptsView.isVisible = !shouldHideReadReceipts
|
holder.readReceiptsView.isVisible = !shouldHideReadReceipts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +65,46 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
|
||||||
super.unbind(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() {
|
class Holder : VectorEpoxyHolder() {
|
||||||
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.style
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.item.AnonymousReadReceipt
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
sealed interface TimelineMessageLayout : Parcelable {
|
sealed interface TimelineMessageLayout : Parcelable {
|
||||||
|
@ -25,11 +26,13 @@ sealed interface TimelineMessageLayout : Parcelable {
|
||||||
val showAvatar: Boolean
|
val showAvatar: Boolean
|
||||||
val showDisplayName: Boolean
|
val showDisplayName: Boolean
|
||||||
val showTimestamp: Boolean
|
val showTimestamp: Boolean
|
||||||
|
val showE2eDecoration: Boolean
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Default(override val showAvatar: Boolean,
|
data class Default(override val showAvatar: Boolean,
|
||||||
override val showDisplayName: Boolean,
|
override val showDisplayName: Boolean,
|
||||||
override val showTimestamp: Boolean,
|
override val showTimestamp: Boolean,
|
||||||
|
override val showE2eDecoration: Boolean,
|
||||||
// Keep defaultLayout generated on epoxy items
|
// Keep defaultLayout generated on epoxy items
|
||||||
override val layoutRes: Int = 0) : TimelineMessageLayout
|
override val layoutRes: Int = 0) : TimelineMessageLayout
|
||||||
|
|
||||||
|
@ -38,6 +41,7 @@ sealed interface TimelineMessageLayout : Parcelable {
|
||||||
override val showAvatar: Boolean,
|
override val showAvatar: Boolean,
|
||||||
override val showDisplayName: Boolean,
|
override val showDisplayName: Boolean,
|
||||||
override val showTimestamp: Boolean = true,
|
override val showTimestamp: Boolean = true,
|
||||||
|
override val showE2eDecoration: Boolean = true,
|
||||||
val isIncoming: Boolean,
|
val isIncoming: Boolean,
|
||||||
val isPseudoBubble: Boolean,
|
val isPseudoBubble: Boolean,
|
||||||
val cornersRadius: CornersRadius,
|
val cornersRadius: CornersRadius,
|
||||||
|
@ -60,22 +64,22 @@ sealed interface TimelineMessageLayout : Parcelable {
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ScBubble(
|
data class ScBubble(
|
||||||
// SC-TODO adapt me
|
|
||||||
override val showAvatar: Boolean,
|
override val showAvatar: Boolean,
|
||||||
override val showDisplayName: Boolean,
|
override val showDisplayName: Boolean,
|
||||||
override val showTimestamp: Boolean = true,
|
override val showTimestamp: Boolean = true,
|
||||||
// SC-TODO?? Keep defaultLayout generated on epoxy items
|
override val showE2eDecoration: Boolean = false,
|
||||||
override val layoutRes: Int = 0
|
|
||||||
/* SC-TODO
|
|
||||||
val isIncoming: Boolean,
|
val isIncoming: Boolean,
|
||||||
|
val reverseBubble: Boolean,
|
||||||
|
val singleSidedLayout: Boolean,
|
||||||
|
val isRealBubble: Boolean,
|
||||||
val isPseudoBubble: Boolean,
|
val isPseudoBubble: Boolean,
|
||||||
|
val isNotice: Boolean,
|
||||||
val timestampAsOverlay: Boolean,
|
val timestampAsOverlay: Boolean,
|
||||||
override val layoutRes: Int = if (isIncoming) {
|
override val layoutRes: Int = if (isIncoming) {
|
||||||
R.layout.item_timeline_event_bubble_incoming_base
|
R.layout.item_timeline_event_sc_bubble_incoming_base
|
||||||
} else {
|
} else {
|
||||||
R.layout.item_timeline_event_bubble_outgoing_base
|
R.layout.item_timeline_event_sc_bubble_outgoing_base
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
) : TimelineMessageLayout
|
) : TimelineMessageLayout
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,11 @@ import im.vector.app.core.resources.LocaleProvider
|
||||||
import im.vector.app.core.resources.isRTL
|
import im.vector.app.core.resources.isRTL
|
||||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
|
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
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.Session
|
||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
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.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.MessageType
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
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 layoutSettingsProvider: TimelineLayoutSettingsProvider,
|
||||||
private val localeProvider: LocaleProvider,
|
private val localeProvider: LocaleProvider,
|
||||||
private val resources: Resources,
|
private val resources: Resources,
|
||||||
|
private val bubbleThemeUtils: BubbleThemeUtils,
|
||||||
private val vectorPreferences: VectorPreferences) {
|
private val vectorPreferences: VectorPreferences) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -96,8 +99,22 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
|
||||||
|
|
||||||
val messageLayout = when (layoutSettingsProvider.getLayoutSettings()) {
|
val messageLayout = when (layoutSettingsProvider.getLayoutSettings()) {
|
||||||
TimelineLayoutSettings.SC_BUBBLE -> {
|
TimelineLayoutSettings.SC_BUBBLE -> {
|
||||||
// SC-TODO?
|
val messageContent = event.getLastMessageContent()
|
||||||
buildModernLayout(showInformation)
|
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 -> {
|
TimelineLayoutSettings.MODERN -> {
|
||||||
buildModernLayout(showInformation)
|
buildModernLayout(showInformation)
|
||||||
|
@ -135,6 +152,35 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
|
||||||
return messageLayout
|
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 {
|
private fun MessageContent?.isPseudoBubble(): Boolean {
|
||||||
if (this == null) return false
|
if (this == null) return false
|
||||||
if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline()
|
if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline()
|
||||||
|
@ -156,19 +202,12 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildModernLayout(showInformation: Boolean): TimelineMessageLayout.Default {
|
private fun buildModernLayout(showInformation: Boolean, forScBubbles: Boolean = false): TimelineMessageLayout.Default {
|
||||||
return TimelineMessageLayout.Default(
|
return TimelineMessageLayout.Default(
|
||||||
showAvatar = showInformation,
|
showAvatar = showInformation,
|
||||||
showDisplayName = showInformation,
|
showDisplayName = showInformation,
|
||||||
showTimestamp = showInformation || vectorPreferences.alwaysShowTimeStamps()
|
showTimestamp = showInformation || vectorPreferences.alwaysShowTimeStamps(),
|
||||||
)
|
showE2eDecoration = !forScBubbles
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildScLayout(showInformation: Boolean): TimelineMessageLayout.ScBubble {
|
|
||||||
return TimelineMessageLayout.ScBubble(
|
|
||||||
showAvatar = showInformation,
|
|
||||||
showDisplayName = showInformation,
|
|
||||||
showTimestamp = true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,10 @@ package im.vector.app.features.home.room.detail.timeline.url
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
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
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
|
|
||||||
interface AbstractPreviewUrlView: TimelineMessageLayoutRenderer {
|
interface AbstractPreviewUrlView {
|
||||||
var isVisible: Boolean
|
var isVisible: Boolean
|
||||||
get() = (this as View).isVisible
|
get() = (this as View).isVisible
|
||||||
set(value) { (this as View).isVisible = value }
|
set(value) { (this as View).isVisible = value }
|
||||||
|
@ -15,4 +15,7 @@ interface AbstractPreviewUrlView: TimelineMessageLayoutRenderer {
|
||||||
fun render(newState: PreviewUrlUiState,
|
fun render(newState: PreviewUrlUiState,
|
||||||
imageContentRenderer: ImageContentRenderer,
|
imageContentRenderer: ImageContentRenderer,
|
||||||
force: Boolean = false)
|
force: Boolean = false)
|
||||||
|
|
||||||
|
// Like upstream TimelineMessageLayoutRenderer, not like downstream one, so don't inherit
|
||||||
|
fun renderMessageLayout(messageLayout: TimelineMessageLayout)
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,8 +35,10 @@ import com.google.android.material.shape.MaterialShapeDrawable
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.resources.LocaleProvider
|
import im.vector.app.core.resources.LocaleProvider
|
||||||
import im.vector.app.core.resources.getLayoutDirectionFromCurrentLocale
|
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.core.utils.DimensionConverter
|
||||||
import im.vector.app.databinding.ViewMessageBubbleBinding
|
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.TimelineMessageLayout
|
||||||
import im.vector.app.features.home.room.detail.timeline.style.shapeAppearanceModel
|
import im.vector.app.features.home.room.detail.timeline.style.shapeAppearanceModel
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
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) {
|
if (messageLayout !is TimelineMessageLayout.Bubble) {
|
||||||
Timber.v("Can't render messageLayout $messageLayout")
|
Timber.v("Can't render messageLayout $messageLayout")
|
||||||
return
|
return
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -16,8 +16,38 @@
|
||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.view
|
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
|
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||||
|
|
||||||
interface TimelineMessageLayoutRenderer {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import android.graphics.Paint
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.AnonymousReadReceipt
|
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 timber.log.Timber
|
||||||
import javax.inject.Inject
|
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_STYLE_BOTH = "both"
|
||||||
const val BUBBLE_TIME_TOP = "top"
|
const val BUBBLE_TIME_TOP = "top"
|
||||||
const val BUBBLE_TIME_BOTTOM = "bottom"
|
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
|
// 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()
|
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
|
/* SC-TODO
|
||||||
fun drawsActualBubbles(bubbleStyle: String): Boolean {
|
fun drawsActualBubbles(bubbleStyle: String): Boolean {
|
||||||
return bubbleStyle == BUBBLE_STYLE_START || bubbleStyle == BUBBLE_STYLE_BOTH
|
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 {
|
fun forceAlwaysShowTimestamps(bubbleStyle: String): Boolean {
|
||||||
return isBubbleTimeLocationSettingAllowed(bubbleStyle)
|
return isBubbleTimeLocationSettingAllowed(bubbleStyle)
|
||||||
}
|
}
|
||||||
|
@ -96,3 +98,17 @@ class BubbleThemeUtils @Inject constructor(private val context: Context) {
|
||||||
return isBubbleTimeLocationSettingAllowed(getBubbleStyle())
|
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())
|
||||||
|
}
|
||||||
|
|
|
@ -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" />
|
|
@ -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" />
|
|
@ -1,13 +1,12 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/eventBaseView"
|
android:id="@+id/eventBaseView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:addStatesFromChildren="true"
|
|
||||||
android:background="?attr/selectableItemBackground"
|
android:background="?attr/selectableItemBackground"
|
||||||
tools:viewBindingIgnore="true">
|
tools:parentTag="android.widget.RelativeLayout">
|
||||||
|
|
||||||
<im.vector.app.core.platform.CheckableView
|
<im.vector.app.core.platform.CheckableView
|
||||||
android:id="@+id/messageSelectedBackground"
|
android:id="@+id/messageSelectedBackground"
|
||||||
|
@ -116,7 +115,7 @@
|
||||||
android:textColor="?vctr_content_primary"
|
android:textColor="?vctr_content_primary"
|
||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
tools:text="@sample/matrix.json/data/displayName" />
|
tools:text="@sample/users.json/data/displayName" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/bubbleMessageTimeView"
|
android:id="@+id/bubbleMessageTimeView"
|
||||||
|
@ -136,7 +135,10 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@id/bubbleMessageTimeView"
|
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" />
|
android:addStatesFromChildren="true" />
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
@ -278,4 +280,4 @@
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</merge>
|
||||||
|
|
Loading…
Reference in New Issue