[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
|
||||
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.children
|
||||
import im.vector.app.features.themes.BubbleThemeUtils
|
||||
import android.content.res.Resources
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
|
||||
|
||||
interface BubbleDependentView<H> {
|
||||
interface BubbleDependentView<H: VectorEpoxyHolder> {
|
||||
|
||||
fun getScBubbleMargin(resources: Resources): Int = resources.getDimensionPixelSize(R.dimen.dual_bubble_one_side_without_avatar_margin)
|
||||
fun getViewStubMinimumWidth(holder: H): Int = 0
|
||||
|
||||
fun allowFooterOverlay(holder: H, bubbleWrapView: ScMessageBubbleWrapView): Boolean = false
|
||||
// Whether to show the footer aligned below the viewStub - requires enough width!
|
||||
fun allowFooterBelow(holder: H): Boolean = true
|
||||
fun needsFooterReservation(): Boolean = false
|
||||
fun reserveFooterSpace(holder: H, width: Int, height: Int) {}
|
||||
fun getInformationData(): MessageInformationData? = null
|
||||
|
||||
// TODO: overwrite for remaining setBubbleLayout()s where necessary: ReadReceiptsItem, MessageImageVideoItem
|
||||
fun applyScBubbleStyle(messageLayout: TimelineMessageLayout.ScBubble, holder: H) {}
|
||||
|
||||
/*
|
||||
fun messageBubbleAllowed(context: Context): Boolean {
|
||||
|
|
|
@ -30,6 +30,7 @@ import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEve
|
|||
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
|
@ -46,6 +47,7 @@ import javax.inject.Inject
|
|||
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val messageLayoutFactory: TimelineMessageLayoutFactory,
|
||||
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||
|
||||
private val collapsedEventIds = linkedSetOf<Long>()
|
||||
|
@ -129,7 +131,8 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||
onCollapsedStateChanged = {
|
||||
mergeItemCollapseStates[event.localId] = it
|
||||
requestModelBuild()
|
||||
}
|
||||
},
|
||||
messageLayout = messageLayoutFactory.createDummy()
|
||||
)
|
||||
MergedMembershipEventsItem_()
|
||||
.id(mergeId)
|
||||
|
@ -206,6 +209,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||
mergeItemCollapseStates[event.localId] = it
|
||||
requestModelBuild()
|
||||
},
|
||||
messageLayout = messageLayoutFactory.createDummy(),
|
||||
hasEncryptionEvent = hasEncryption,
|
||||
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
callback = callback,
|
||||
|
|
|
@ -21,10 +21,12 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
|||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory
|
||||
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer) {
|
||||
class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer,
|
||||
private val messageLayoutFactory: TimelineMessageLayoutFactory) {
|
||||
|
||||
fun create(
|
||||
eventId: String,
|
||||
|
@ -44,6 +46,7 @@ class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: Av
|
|||
.id("read_receipts_$eventId")
|
||||
.eventId(eventId)
|
||||
.readReceipts(readReceiptsData)
|
||||
.messageLayout(messageLayoutFactory.createDummy())
|
||||
.avatarRenderer(avatarRenderer)
|
||||
.shouldHideReadReceipts(isFromThreadTimeLine)
|
||||
.clickListener {
|
||||
|
|
|
@ -161,16 +161,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||
ReferencesInfoData(verificationState)
|
||||
},
|
||||
sentByMe = isSentByMe,
|
||||
readReceiptAnonymous = if (event.root.sendState == SendState.SYNCED || event.root.sendState == SendState.SENT) {
|
||||
/*if (event.readByOther) {
|
||||
AnonymousReadReceipt.READ
|
||||
} else {
|
||||
AnonymousReadReceipt.SENT
|
||||
}*/
|
||||
AnonymousReadReceipt.NONE
|
||||
} else {
|
||||
AnonymousReadReceipt.PROCESSING
|
||||
},
|
||||
readReceiptAnonymous = BubbleThemeUtils.anonymousReadReceiptForEvent(event),
|
||||
senderPowerLevel = senderPowerLevel,
|
||||
isDirect = isEffectivelyDirect,
|
||||
isPublic = roomSummary?.isPublic ?: false,
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
|
@ -25,13 +26,19 @@ import androidx.core.view.isVisible
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.ui.views.BubbleDependentView
|
||||
import im.vector.app.core.ui.views.ShieldImageView
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.view.scRenderMessageLayout
|
||||
import im.vector.app.features.reactions.widget.ReactionButton
|
||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
* Base timeline item with reactions and read receipts.
|
||||
|
@ -85,23 +92,22 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
|
|||
holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener)
|
||||
}
|
||||
|
||||
// SchildiChat: moved to setBubbleLayout() (called from super.bind()) - so we can do this bubble-style-specific
|
||||
/*
|
||||
when (baseAttributes.informationData.e2eDecoration) {
|
||||
E2EDecoration.NONE -> {
|
||||
holder.e2EDecorationView.render(null)
|
||||
}
|
||||
E2EDecoration.WARN_IN_CLEAR,
|
||||
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
|
||||
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
|
||||
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
|
||||
if (baseAttributes.informationData.messageLayout.showE2eDecoration) {
|
||||
when (baseAttributes.informationData.e2eDecoration) {
|
||||
E2EDecoration.NONE -> {
|
||||
holder.e2EDecorationView.render(null)
|
||||
}
|
||||
E2EDecoration.WARN_IN_CLEAR,
|
||||
E2EDecoration.WARN_SENT_BY_UNVERIFIED,
|
||||
E2EDecoration.WARN_SENT_BY_UNKNOWN -> {
|
||||
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
holder.view.onClick(baseAttributes.itemClickListener)
|
||||
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
|
||||
(holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout)
|
||||
(holder.view as? TimelineMessageLayoutRenderer).scRenderMessageLayout(baseAttributes.informationData.messageLayout, this, holder)
|
||||
}
|
||||
|
||||
override fun unbind(holder: H) {
|
||||
|
@ -116,6 +122,25 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
|
|||
failureIndicator?.isVisible = baseAttributes.informationData.sendState.hasFailed()
|
||||
}
|
||||
|
||||
override fun getScBubbleMargin(resources: Resources): Int {
|
||||
return when {
|
||||
(baseAttributes.informationData.messageLayout as? TimelineMessageLayout.ScBubble)?.singleSidedLayout == true -> 0
|
||||
// else: dual-side bubbles (getBubbleMargin should not get called for other bubbleStyles)
|
||||
|
||||
// Direct chats usually have avatars hidden on both sides
|
||||
baseAttributes.informationData.isDirect -> resources.getDimensionPixelSize(R.dimen.dual_bubble_both_sides_without_avatar_margin)
|
||||
// No direct chat, but sent by me: other side has an avatar
|
||||
baseAttributes.informationData.sentByMe -> {
|
||||
resources.getDimensionPixelSize(R.dimen.dual_bubble_one_side_without_avatar_margin) +
|
||||
resources.getDimensionPixelSize(R.dimen.dual_bubble_one_side_avatar_offset) +
|
||||
// SC bubbles use SMALL avatars
|
||||
ceil(AvatarSizeProvider.Companion.AvatarStyle.SMALL.avatarSizeDP * resources.displayMetrics.density).toInt()
|
||||
}
|
||||
// No direct chat, sent by other: my side has hidden avatar
|
||||
else -> resources.getDimensionPixelSize(R.dimen.dual_bubble_one_side_without_avatar_margin)
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
|
||||
val reactionsContainer by bind<ViewGroup>(R.id.reactionsContainer)
|
||||
val informationBottom by bind<ViewGroup>(R.id.informationBottom)
|
||||
|
|
|
@ -36,8 +36,15 @@ import im.vector.app.features.home.AvatarRenderer
|
|||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
|
||||
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.view.canHideAvatars
|
||||
import im.vector.app.features.home.room.detail.timeline.view.infoInBubbles
|
||||
import im.vector.app.features.themes.guessTextWidth
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadDetails
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
* Base timeline item that adds an optional information bar with the sender avatar, name, time, send state
|
||||
|
@ -75,40 +82,42 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||
|
||||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
if (attributes.informationData.messageLayout.showAvatar) {
|
||||
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
|
||||
height = attributes.avatarSize
|
||||
width = attributes.avatarSize
|
||||
if ((holder.view as? ScMessageBubbleWrapView)?.customBind(this, holder, attributes, _avatarClickListener, _memberNameClickListener) != true) {
|
||||
if (attributes.informationData.messageLayout.showAvatar) {
|
||||
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
|
||||
height = attributes.avatarSize
|
||||
width = attributes.avatarSize
|
||||
}
|
||||
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
|
||||
holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.avatarImageView.isVisible = true
|
||||
holder.avatarImageView.onClick(_avatarClickListener)
|
||||
} else {
|
||||
holder.avatarImageView.setOnClickListener(null)
|
||||
holder.avatarImageView.setOnLongClickListener(null)
|
||||
holder.avatarImageView.isVisible = false
|
||||
}
|
||||
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
|
||||
holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.avatarImageView.isVisible = true
|
||||
holder.avatarImageView.onClick(_avatarClickListener)
|
||||
} else {
|
||||
holder.avatarImageView.setOnClickListener(null)
|
||||
holder.avatarImageView.setOnLongClickListener(null)
|
||||
holder.avatarImageView.isVisible = false
|
||||
if (attributes.informationData.messageLayout.showDisplayName) {
|
||||
holder.memberNameView.isVisible = true
|
||||
holder.memberNameView.text = attributes.informationData.memberName
|
||||
holder.memberNameView.setTextColor(attributes.getMemberNameColor())
|
||||
holder.memberNameView.onClick(_memberNameClickListener)
|
||||
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
} else {
|
||||
holder.memberNameView.setOnClickListener(null)
|
||||
holder.memberNameView.setOnLongClickListener(null)
|
||||
holder.memberNameView.isVisible = false
|
||||
}
|
||||
if (attributes.informationData.messageLayout.showTimestamp) {
|
||||
holder.timeView.isVisible = true
|
||||
holder.timeView.text = attributes.informationData.time
|
||||
} else {
|
||||
holder.timeView.isVisible = false
|
||||
}
|
||||
// Render send state indicator
|
||||
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
|
||||
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
|
||||
}
|
||||
if (attributes.informationData.messageLayout.showDisplayName) {
|
||||
holder.memberNameView.isVisible = true
|
||||
holder.memberNameView.text = attributes.informationData.memberName
|
||||
holder.memberNameView.setTextColor(attributes.getMemberNameColor())
|
||||
holder.memberNameView.onClick(_memberNameClickListener)
|
||||
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
} else {
|
||||
holder.memberNameView.setOnClickListener(null)
|
||||
holder.memberNameView.setOnLongClickListener(null)
|
||||
holder.memberNameView.isVisible = false
|
||||
}
|
||||
if (attributes.informationData.messageLayout.showTimestamp) {
|
||||
holder.timeView.isVisible = true
|
||||
holder.timeView.text = attributes.informationData.time
|
||||
} else {
|
||||
holder.timeView.isVisible = false
|
||||
}
|
||||
// Render send state indicator
|
||||
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
|
||||
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
|
||||
|
||||
// Threads
|
||||
if (attributes.areThreadMessagesEnabled) {
|
||||
|
@ -151,14 +160,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||
super.unbind(holder)
|
||||
}
|
||||
|
||||
private fun Attributes.getMemberNameColor() = messageColorProvider.getMemberNameTextColor(
|
||||
informationData.matrixItem,
|
||||
MatrixItemColorProvider.UserInRoomInformation(
|
||||
attributes.informationData.isDirect,
|
||||
attributes.informationData.isPublic,
|
||||
attributes.informationData.senderPowerLevel
|
||||
)
|
||||
)
|
||||
override fun getInformationData(): MessageInformationData? = attributes.informationData
|
||||
|
||||
abstract class Holder(@IdRes stubId: Int) : AbsBaseMessageItem.Holder(stubId) {
|
||||
|
||||
|
@ -216,10 +218,20 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||
|
||||
return result
|
||||
}
|
||||
|
||||
fun getMemberNameColor() = messageColorProvider.getMemberNameTextColor(
|
||||
informationData.matrixItem,
|
||||
MatrixItemColorProvider.UserInRoomInformation(
|
||||
informationData.isDirect,
|
||||
informationData.isPublic,
|
||||
informationData.senderPowerLevel
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun ignoreMessageGuideline(context: Context): Boolean {
|
||||
return false // SC-TODO infoInBubbles(context) && canHideAvatars()
|
||||
val messageLayout = attributes.informationData.messageLayout as? TimelineMessageLayout.ScBubble ?: return false
|
||||
return infoInBubbles(messageLayout) && canHideAvatars(attributes)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.item
|
|||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewStub
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.annotation.IdRes
|
||||
|
@ -28,6 +29,7 @@ import im.vector.app.core.epoxy.VectorEpoxyHolder
|
|||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.platform.CheckableView
|
||||
import im.vector.app.core.ui.views.BubbleDependentView
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
|
||||
/**
|
||||
* Children must override getViewType()
|
||||
|
@ -71,6 +73,7 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||
val leftGuideline by bind<View>(R.id.messageStartGuideline)
|
||||
val contentContainer by bind<View>(R.id.viewStubContainer)
|
||||
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
||||
val viewStubContainer by bind<FrameLayout>(R.id.viewStubContainer)
|
||||
|
||||
override fun bindView(itemView: View) {
|
||||
super.bindView(itemView)
|
||||
|
|
|
@ -21,6 +21,9 @@ import android.widget.TextView
|
|||
import androidx.annotation.IdRes
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.view.scOnlyRenderMessageLayout
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>() {
|
||||
|
@ -39,6 +42,8 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
|
|||
holder.separatorView.visibility = View.VISIBLE
|
||||
holder.expandView.setText(R.string.merged_events_collapse)
|
||||
}
|
||||
|
||||
(holder.view as? TimelineMessageLayoutRenderer).scOnlyRenderMessageLayout(attributes.messageLayout, this, holder)
|
||||
}
|
||||
|
||||
protected val distinctMergeData by lazy {
|
||||
|
@ -69,6 +74,7 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
|
|||
val mergeData: List<Data>
|
||||
val avatarRenderer: AvatarRenderer
|
||||
val onCollapsedStateChanged: (Boolean) -> Unit
|
||||
val messageLayout: TimelineMessageLayout
|
||||
}
|
||||
|
||||
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
|
||||
|
|
|
@ -23,6 +23,8 @@ import com.airbnb.epoxy.EpoxyAttribute
|
|||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.view.scOnlyRenderMessageLayout
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||
abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
|
||||
|
@ -35,6 +37,8 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
|
|||
holder.messageTextView.text = attributes.text
|
||||
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
|
||||
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
|
||||
(holder.view as? TimelineMessageLayoutRenderer).scOnlyRenderMessageLayout(attributes.informationData.messageLayout, this, holder)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
|
|
|
@ -25,6 +25,7 @@ import com.airbnb.epoxy.EpoxyAttribute
|
|||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
|
||||
|
@ -69,6 +70,7 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
|
|||
override val isCollapsed: Boolean,
|
||||
override val mergeData: List<Data>,
|
||||
override val avatarRenderer: AvatarRenderer,
|
||||
override val onCollapsedStateChanged: (Boolean) -> Unit
|
||||
override val onCollapsedStateChanged: (Boolean) -> Unit,
|
||||
override val messageLayout: TimelineMessageLayout
|
||||
) : BasedMergedItem.Attributes
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import im.vector.app.core.utils.tappableMatchingText
|
|||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
|
@ -224,6 +225,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
|||
override val mergeData: List<Data>,
|
||||
override val avatarRenderer: AvatarRenderer,
|
||||
override val onCollapsedStateChanged: (Boolean) -> Unit,
|
||||
override val messageLayout: TimelineMessageLayout,
|
||||
val callback: TimelineEventController.Callback? = null,
|
||||
val currentUserId: String,
|
||||
val hasEncryptionEvent: Boolean,
|
||||
|
|
|
@ -33,6 +33,9 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadSt
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import im.vector.app.features.themes.guessTextWidth
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||
|
@ -103,6 +106,18 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
|||
contentDownloadStateTrackerBinder.unbind(mxcUrl)
|
||||
}
|
||||
|
||||
override fun getViewStubMinimumWidth(holder: Holder): Int {
|
||||
// Guess text width for name and time
|
||||
// On first call, holder.fileImageView.width is not initialized yet
|
||||
val imageWidth = holder.fileImageView.resources.getDimensionPixelSize(R.dimen.chat_avatar_size)
|
||||
val minimumWidthWithText =
|
||||
ceil(guessTextWidth(holder.filenameView, filename)).toInt() +
|
||||
imageWidth +
|
||||
holder.filenameView.resources.getDimensionPixelSize(R.dimen.sc_bubble_guess_minimum_width_padding)
|
||||
val absoluteMinimumWidth = imageWidth*3
|
||||
return max(absoluteMinimumWidth, minimumWidthWithText)
|
||||
}
|
||||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
|
|
|
@ -16,9 +16,11 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
|
@ -33,6 +35,7 @@ import im.vector.app.core.utils.DimensionConverter
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
|
||||
import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import org.matrix.android.sdk.api.util.MimeTypes
|
||||
|
||||
|
@ -57,7 +60,6 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||
@EpoxyAttribute
|
||||
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
|
||||
|
||||
// SC-TODO
|
||||
var lastAllowedFooterOverlay: Boolean = true
|
||||
var lastShowFooterBellow: Boolean = true
|
||||
var forceAllowFooterOverlay: Boolean? = null
|
||||
|
@ -67,18 +69,19 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||
forceAllowFooterOverlay = null
|
||||
super.bind(holder)
|
||||
|
||||
val bubbleWrapView = (holder.view as? ScMessageBubbleWrapView)
|
||||
val host = this
|
||||
val onImageSizeListener = object: ImageContentRenderer.OnImageSizeListener {
|
||||
override fun onImageSizeUpdated(width: Int, height: Int) {
|
||||
bubbleWrapView ?: return
|
||||
// Image size change -> different footer space situation possible
|
||||
/* SC-TODO
|
||||
val footerMeasures = getFooterMeasures(holder)
|
||||
val footerMeasures = bubbleWrapView.getFooterMeasures(attributes.informationData)
|
||||
forceAllowFooterOverlay = shouldAllowFooterOverlay(footerMeasures, width, height)
|
||||
val newShowFooterBellow = shouldShowFooterBellow(footerMeasures, width, height)
|
||||
if (lastAllowedFooterOverlay != forceAllowFooterOverlay || newShowFooterBellow != lastShowFooterBellow) {
|
||||
showFooterBellow = newShowFooterBellow
|
||||
updateMessageBubble(holder.imageView.context, holder)
|
||||
bubbleWrapView.renderMessageLayout(attributes.informationData.messageLayout, host, holder)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
val animate = mediaData.mimeType == MimeTypes.Gif
|
||||
|
@ -87,11 +90,10 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||
|
||||
val messageLayout = baseAttributes.informationData.messageLayout
|
||||
val dimensionConverter = DimensionConverter(holder.view.resources)
|
||||
// SC-TODO handle SC bubbles
|
||||
val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
|
||||
messageLayout.cornersRadius.granularRoundedCorners()
|
||||
} else {
|
||||
RoundedCorners(dimensionConverter.dpToPx(8))
|
||||
val imageCornerTransformation = when (messageLayout) {
|
||||
is TimelineMessageLayout.ScBubble -> RoundedCorners(dimensionConverter.dpToPx(3))
|
||||
is TimelineMessageLayout.Bubble -> messageLayout.cornersRadius.granularRoundedCorners()
|
||||
else -> RoundedCorners(dimensionConverter.dpToPx(8))
|
||||
}
|
||||
imageContentRenderer.render(mediaData, effectiveMode, holder.imageView, imageCornerTransformation, onImageSizeListener)
|
||||
if (!attributes.informationData.sendState.hasFailed()) {
|
||||
|
@ -122,7 +124,6 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
// SC-TODO
|
||||
private fun shouldAllowFooterOverlay(footerMeasures: Array<Int>, imageWidth: Int, imageHeight: Int): Boolean {
|
||||
val footerWidth = footerMeasures[0]
|
||||
val footerHeight = footerMeasures[1]
|
||||
|
@ -131,7 +132,6 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||
return imageWidth > 1.5*footerWidth && imageHeight > 1.5*footerHeight && (imageWidth * imageHeight > 4 * footerWidth * footerHeight)
|
||||
}
|
||||
|
||||
// SC-TODO
|
||||
private fun shouldShowFooterBellow(footerMeasures: Array<Int>, imageWidth: Int, imageHeight: Int): Boolean {
|
||||
// Only show footer bellow if the width is not the limiting factor (or it will get cut).
|
||||
// Otherwise, we can not be sure in this place that we'll have enough space on the side
|
||||
|
@ -141,6 +141,57 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||
return imageWidth > 1.5*footerWidth && imageHeight < 1.5*footerHeight
|
||||
}
|
||||
|
||||
override fun allowFooterOverlay(holder: Holder, bubbleWrapView: ScMessageBubbleWrapView): Boolean {
|
||||
val rememberedAllowFooterOverlay = forceAllowFooterOverlay
|
||||
if (rememberedAllowFooterOverlay != null) {
|
||||
lastAllowedFooterOverlay = rememberedAllowFooterOverlay
|
||||
return rememberedAllowFooterOverlay
|
||||
}
|
||||
val imageWidth = holder.imageView.width
|
||||
val imageHeight = holder.imageView.height
|
||||
if (imageWidth == 0 && imageHeight == 0) {
|
||||
// Not initialised yet, assume true
|
||||
lastAllowedFooterOverlay = true
|
||||
return true
|
||||
}
|
||||
// If the footer covers most of the image, or is even larger than the image, move it outside
|
||||
val footerMeasures = bubbleWrapView.getFooterMeasures(baseAttributes.informationData)
|
||||
lastAllowedFooterOverlay = shouldAllowFooterOverlay(footerMeasures, imageWidth, imageHeight)
|
||||
return lastAllowedFooterOverlay
|
||||
}
|
||||
|
||||
override fun allowFooterBelow(holder: Holder): Boolean {
|
||||
val showBellow = showFooterBellow
|
||||
lastShowFooterBellow = showBellow
|
||||
return showBellow
|
||||
}
|
||||
|
||||
override fun getScBubbleMargin(resources: Resources): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun applyScBubbleStyle(messageLayout: TimelineMessageLayout.ScBubble, holder: Holder) {
|
||||
// Case: ImageContentRenderer.processSize only sees width=height=0 -> width of the ImageView not adapted to the actual content
|
||||
// -> Align image within ImageView to same side as message bubbles
|
||||
holder.imageView.scaleType = if (messageLayout.reverseBubble) ImageView.ScaleType.FIT_END else ImageView.ScaleType.FIT_START
|
||||
// Case: Message information (sender name + date) makes the containing view wider than the ImageView
|
||||
// -> Align ImageView within its parent to the same side as message bubbles
|
||||
(holder.imageView.layoutParams as ConstraintLayout.LayoutParams).horizontalBias = if (messageLayout.reverseBubble) 1f else 0f
|
||||
|
||||
// Image outline
|
||||
when {
|
||||
!(messageLayout.isRealBubble || messageLayout.isPseudoBubble) || mode != ImageContentRenderer.Mode.THUMBNAIL -> {
|
||||
// Don't show it for non-bubble layouts, don't show for Stickers, ...
|
||||
holder.mediaContentView.background = null
|
||||
}
|
||||
attributes.informationData.sentByMe -> {
|
||||
holder.mediaContentView.setBackgroundResource(R.drawable.background_image_border_outgoing)
|
||||
}
|
||||
else -> {
|
||||
holder.mediaContentView.setBackgroundResource(R.drawable.background_image_border_incoming)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
|
||||
|
|
|
@ -35,6 +35,7 @@ import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
|||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlUiState
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlViewSc
|
||||
import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
||||
import io.noties.markwon.MarkwonPlugin
|
||||
|
@ -174,6 +175,32 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
override fun allowFooterOverlay(holder: Holder, bubbleWrapView: ScMessageBubbleWrapView): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun needsFooterReservation(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun reserveFooterSpace(holder: Holder, width: Int, height: Int) {
|
||||
// Remember for PreviewUrlViewUpdater.onStateUpdated
|
||||
footerWidth = width
|
||||
footerHeight = height
|
||||
// Reserve both in preview and in message
|
||||
// User might close preview, so we still need place in the message
|
||||
// if we don't want to change this afterwards
|
||||
// This might be a race condition, but the UI-isssue if evaluated wrongly is negligible
|
||||
if (!holder.previewUrlView.isVisible) {
|
||||
holder.messageView.footerWidth = width
|
||||
holder.messageView.footerHeight = height
|
||||
} // else: will be handled in onStateUpdated
|
||||
holder.previewUrlViewSc.footerWidth = height
|
||||
holder.previewUrlViewSc.footerHeight = height
|
||||
holder.previewUrlViewSc.updateFooterSpace()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageContentTextStub
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import im.vector.app.core.epoxy.onClick
|
|||
import im.vector.app.core.ui.views.ShieldImageView
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.view.scOnlyRenderMessageLayout
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||
|
||||
|
@ -53,6 +55,8 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
|||
holder.e2EDecorationView.render(RoomEncryptionTrustLevel.Warning)
|
||||
}
|
||||
}
|
||||
|
||||
(holder.view as? TimelineMessageLayoutRenderer).scOnlyRenderMessageLayout(attributes.informationData.messageLayout, this, holder)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
|
@ -24,15 +27,21 @@ import im.vector.app.R
|
|||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.resources.LocaleProvider
|
||||
import im.vector.app.core.resources.getLayoutDirectionFromCurrentLocale
|
||||
import im.vector.app.core.ui.views.BubbleDependentView
|
||||
import im.vector.app.core.ui.views.ReadReceiptsView
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.view.ScMessageBubbleWrapView
|
||||
import im.vector.app.features.home.room.detail.timeline.view.setFlatRtl
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_read_receipts)
|
||||
abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(), ItemWithEvents, BubbleDependentView<ReadReceiptsItem.Holder> {
|
||||
|
||||
@EpoxyAttribute lateinit var eventId: String
|
||||
@EpoxyAttribute lateinit var readReceipts: List<ReadReceiptData>
|
||||
@EpoxyAttribute lateinit var messageLayout: TimelineMessageLayout
|
||||
@EpoxyAttribute var shouldHideReadReceipts: Boolean = false
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: ClickListener
|
||||
|
@ -46,6 +55,8 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
|
|||
holder.readReceiptsView.onClick(clickListener)
|
||||
holder.readReceiptsView.render(readReceipts, avatarRenderer)
|
||||
|
||||
(messageLayout as? TimelineMessageLayout.ScBubble)?.let { applyScBubbleStyle(it, holder) }
|
||||
|
||||
holder.readReceiptsView.isVisible = !shouldHideReadReceipts
|
||||
}
|
||||
|
||||
|
@ -54,6 +65,46 @@ abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(
|
|||
super.unbind(holder)
|
||||
}
|
||||
|
||||
override fun applyScBubbleStyle(messageLayout: TimelineMessageLayout.ScBubble, holder: Holder) {
|
||||
val defaultDirection = LocaleProvider(holder.view.resources).getLayoutDirectionFromCurrentLocale()
|
||||
val defaultRtl = defaultDirection == View.LAYOUT_DIRECTION_RTL
|
||||
val reverseDirection = if (defaultRtl) View.LAYOUT_DIRECTION_LTR else View.LAYOUT_DIRECTION_RTL
|
||||
|
||||
/*
|
||||
val receiptParent = holder.readReceiptsView.parent
|
||||
if (receiptParent is LinearLayout) {
|
||||
(holder.readReceiptsView.layoutParams as LinearLayout.LayoutParams).gravity = if (dualBubbles) Gravity.START else Gravity.END
|
||||
|
||||
(receiptParent.layoutParams as RelativeLayout.LayoutParams).removeRule(RelativeLayout.END_OF)
|
||||
(receiptParent.layoutParams as RelativeLayout.LayoutParams).removeRule(RelativeLayout.ALIGN_PARENT_START)
|
||||
if (dualBubbles) {
|
||||
(receiptParent.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.ALIGN_PARENT_START, RelativeLayout.TRUE)
|
||||
} else {
|
||||
(receiptParent.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.END_OF, R.id.messageStartGuideline)
|
||||
}
|
||||
} else if (receiptParent is RelativeLayout) {
|
||||
if (dualBubbles) {
|
||||
(holder.readReceiptsView.layoutParams as RelativeLayout.LayoutParams).removeRule(RelativeLayout.ALIGN_PARENT_END)
|
||||
} else {
|
||||
(holder.readReceiptsView.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.ALIGN_PARENT_END)
|
||||
}
|
||||
} else if (receiptParent is FrameLayout) {
|
||||
*/
|
||||
if (messageLayout.singleSidedLayout) {
|
||||
(holder.readReceiptsView.layoutParams as FrameLayout.LayoutParams).gravity = Gravity.END
|
||||
} else {
|
||||
(holder.readReceiptsView.layoutParams as FrameLayout.LayoutParams).gravity = Gravity.START
|
||||
}
|
||||
/*
|
||||
} else {
|
||||
Timber.e("Unsupported layout for read receipts parent: $receiptParent")
|
||||
}
|
||||
*/
|
||||
|
||||
// Also set rtl to have members fill from the natural side
|
||||
setFlatRtl(holder.readReceiptsView, if (messageLayout.singleSidedLayout) defaultDirection else reverseDirection, defaultDirection)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.style
|
|||
|
||||
import android.os.Parcelable
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AnonymousReadReceipt
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
sealed interface TimelineMessageLayout : Parcelable {
|
||||
|
@ -25,11 +26,13 @@ sealed interface TimelineMessageLayout : Parcelable {
|
|||
val showAvatar: Boolean
|
||||
val showDisplayName: Boolean
|
||||
val showTimestamp: Boolean
|
||||
val showE2eDecoration: Boolean
|
||||
|
||||
@Parcelize
|
||||
data class Default(override val showAvatar: Boolean,
|
||||
override val showDisplayName: Boolean,
|
||||
override val showTimestamp: Boolean,
|
||||
override val showE2eDecoration: Boolean,
|
||||
// Keep defaultLayout generated on epoxy items
|
||||
override val layoutRes: Int = 0) : TimelineMessageLayout
|
||||
|
||||
|
@ -38,6 +41,7 @@ sealed interface TimelineMessageLayout : Parcelable {
|
|||
override val showAvatar: Boolean,
|
||||
override val showDisplayName: Boolean,
|
||||
override val showTimestamp: Boolean = true,
|
||||
override val showE2eDecoration: Boolean = true,
|
||||
val isIncoming: Boolean,
|
||||
val isPseudoBubble: Boolean,
|
||||
val cornersRadius: CornersRadius,
|
||||
|
@ -60,22 +64,22 @@ sealed interface TimelineMessageLayout : Parcelable {
|
|||
|
||||
@Parcelize
|
||||
data class ScBubble(
|
||||
// SC-TODO adapt me
|
||||
override val showAvatar: Boolean,
|
||||
override val showDisplayName: Boolean,
|
||||
override val showTimestamp: Boolean = true,
|
||||
// SC-TODO?? Keep defaultLayout generated on epoxy items
|
||||
override val layoutRes: Int = 0
|
||||
/* SC-TODO
|
||||
override val showE2eDecoration: Boolean = false,
|
||||
val isIncoming: Boolean,
|
||||
val reverseBubble: Boolean,
|
||||
val singleSidedLayout: Boolean,
|
||||
val isRealBubble: Boolean,
|
||||
val isPseudoBubble: Boolean,
|
||||
val isNotice: Boolean,
|
||||
val timestampAsOverlay: Boolean,
|
||||
override val layoutRes: Int = if (isIncoming) {
|
||||
R.layout.item_timeline_event_bubble_incoming_base
|
||||
R.layout.item_timeline_event_sc_bubble_incoming_base
|
||||
} else {
|
||||
R.layout.item_timeline_event_bubble_outgoing_base
|
||||
R.layout.item_timeline_event_sc_bubble_outgoing_base
|
||||
}
|
||||
*/
|
||||
) : TimelineMessageLayout
|
||||
|
||||
}
|
||||
|
|
|
@ -23,9 +23,11 @@ import im.vector.app.core.resources.LocaleProvider
|
|||
import im.vector.app.core.resources.isRTL
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.themes.BubbleThemeUtils
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageNoticeContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
|
@ -37,6 +39,7 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
|
|||
private val layoutSettingsProvider: TimelineLayoutSettingsProvider,
|
||||
private val localeProvider: LocaleProvider,
|
||||
private val resources: Resources,
|
||||
private val bubbleThemeUtils: BubbleThemeUtils,
|
||||
private val vectorPreferences: VectorPreferences) {
|
||||
|
||||
companion object {
|
||||
|
@ -96,8 +99,22 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
|
|||
|
||||
val messageLayout = when (layoutSettingsProvider.getLayoutSettings()) {
|
||||
TimelineLayoutSettings.SC_BUBBLE -> {
|
||||
// SC-TODO?
|
||||
buildModernLayout(showInformation)
|
||||
val messageContent = event.getLastMessageContent()
|
||||
val isBubble = event.shouldBuildBubbleLayout()
|
||||
val singleSidedLayout = bubbleThemeUtils.getBubbleStyle() == BubbleThemeUtils.BUBBLE_STYLE_START
|
||||
val pseudoBubble = messageContent.isPseudoBubble()
|
||||
return TimelineMessageLayout.ScBubble(
|
||||
showAvatar = showInformation,
|
||||
showDisplayName = showInformation,
|
||||
showTimestamp = !singleSidedLayout || vectorPreferences.alwaysShowTimeStamps(),
|
||||
isIncoming = !isSentByMe,
|
||||
isNotice = messageContent is MessageNoticeContent,
|
||||
reverseBubble = isSentByMe && !singleSidedLayout,
|
||||
singleSidedLayout = singleSidedLayout,
|
||||
isRealBubble = isBubble && !pseudoBubble,
|
||||
isPseudoBubble = pseudoBubble,
|
||||
timestampAsOverlay = messageContent.timestampAsOverlay()
|
||||
)
|
||||
}
|
||||
TimelineLayoutSettings.MODERN -> {
|
||||
buildModernLayout(showInformation)
|
||||
|
@ -135,6 +152,35 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
|
|||
return messageLayout
|
||||
}
|
||||
|
||||
/**
|
||||
* Just a dumb layout setting, so we get basic ScBubble settings in strictly-non-bubble classes as well
|
||||
*/
|
||||
fun createDummy(): TimelineMessageLayout {
|
||||
return when (layoutSettingsProvider.getLayoutSettings()) {
|
||||
TimelineLayoutSettings.SC_BUBBLE -> {
|
||||
val singleSidedLayout = bubbleThemeUtils.getBubbleStyle() == BubbleThemeUtils.BUBBLE_STYLE_START
|
||||
return TimelineMessageLayout.ScBubble(
|
||||
showAvatar = false,
|
||||
showDisplayName = false,
|
||||
showTimestamp = true,
|
||||
isIncoming = false,
|
||||
isNotice = false,
|
||||
reverseBubble = false,
|
||||
singleSidedLayout = singleSidedLayout,
|
||||
isRealBubble = false,
|
||||
isPseudoBubble = false,
|
||||
timestampAsOverlay = false
|
||||
)
|
||||
}
|
||||
else -> TimelineMessageLayout.Default(
|
||||
showAvatar = false,
|
||||
showDisplayName = false,
|
||||
showTimestamp = vectorPreferences.alwaysShowTimeStamps(),
|
||||
showE2eDecoration = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessageContent?.isPseudoBubble(): Boolean {
|
||||
if (this == null) return false
|
||||
if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline()
|
||||
|
@ -156,19 +202,12 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess
|
|||
return false
|
||||
}
|
||||
|
||||
private fun buildModernLayout(showInformation: Boolean): TimelineMessageLayout.Default {
|
||||
private fun buildModernLayout(showInformation: Boolean, forScBubbles: Boolean = false): TimelineMessageLayout.Default {
|
||||
return TimelineMessageLayout.Default(
|
||||
showAvatar = showInformation,
|
||||
showDisplayName = showInformation,
|
||||
showTimestamp = showInformation || vectorPreferences.alwaysShowTimeStamps()
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildScLayout(showInformation: Boolean): TimelineMessageLayout.ScBubble {
|
||||
return TimelineMessageLayout.ScBubble(
|
||||
showAvatar = showInformation,
|
||||
showDisplayName = showInformation,
|
||||
showTimestamp = true
|
||||
showTimestamp = showInformation || vectorPreferences.alwaysShowTimeStamps(),
|
||||
showE2eDecoration = !forScBubbles
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@ package im.vector.app.features.home.room.detail.timeline.url
|
|||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
|
||||
interface AbstractPreviewUrlView: TimelineMessageLayoutRenderer {
|
||||
interface AbstractPreviewUrlView {
|
||||
var isVisible: Boolean
|
||||
get() = (this as View).isVisible
|
||||
set(value) { (this as View).isVisible = value }
|
||||
|
@ -15,4 +15,7 @@ interface AbstractPreviewUrlView: TimelineMessageLayoutRenderer {
|
|||
fun render(newState: PreviewUrlUiState,
|
||||
imageContentRenderer: ImageContentRenderer,
|
||||
force: Boolean = false)
|
||||
|
||||
// Like upstream TimelineMessageLayoutRenderer, not like downstream one, so don't inherit
|
||||
fun renderMessageLayout(messageLayout: TimelineMessageLayout)
|
||||
}
|
||||
|
|
|
@ -35,8 +35,10 @@ import com.google.android.material.shape.MaterialShapeDrawable
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.LocaleProvider
|
||||
import im.vector.app.core.resources.getLayoutDirectionFromCurrentLocale
|
||||
import im.vector.app.core.ui.views.BubbleDependentView
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.databinding.ViewMessageBubbleBinding
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.style.shapeAppearanceModel
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
|
@ -93,7 +95,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri
|
|||
}
|
||||
}
|
||||
|
||||
override fun renderMessageLayout(messageLayout: TimelineMessageLayout) {
|
||||
override fun <H: BaseEventItem.BaseHolder>renderMessageLayout(messageLayout: TimelineMessageLayout, bubbleDependentView: BubbleDependentView<H>, holder: H) {
|
||||
if (messageLayout !is TimelineMessageLayout.Bubble) {
|
||||
Timber.v("Can't render messageLayout $messageLayout")
|
||||
return
|
||||
|
|
|
@ -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
|
||||
|
||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.ui.views.BubbleDependentView
|
||||
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
|
||||
interface TimelineMessageLayoutRenderer {
|
||||
fun renderMessageLayout(messageLayout: TimelineMessageLayout)
|
||||
fun <H: BaseEventItem.BaseHolder>renderMessageLayout(messageLayout: TimelineMessageLayout,
|
||||
bubbleDependentView: BubbleDependentView<H>,
|
||||
holder: H)
|
||||
|
||||
// Variant to use from classes that do not use BaseEventItem.BaseHolder, and don't need the heavy bubble stuff
|
||||
fun <H: VectorEpoxyHolder>renderBaseMessageLayout(messageLayout: TimelineMessageLayout,
|
||||
bubbleDependentView: BubbleDependentView<H>,
|
||||
holder: H) {}
|
||||
}
|
||||
|
||||
// Only render message layout for SC layouts - even if parent is not an ScBubble
|
||||
fun <H: BaseEventItem.BaseHolder>TimelineMessageLayoutRenderer?.scOnlyRenderMessageLayout(messageLayout: TimelineMessageLayout,
|
||||
bubbleDependentView: BubbleDependentView<H>,
|
||||
holder: H) {
|
||||
if (messageLayout is TimelineMessageLayout.ScBubble) {
|
||||
scRenderMessageLayout(messageLayout, bubbleDependentView, holder)
|
||||
}
|
||||
}
|
||||
|
||||
// Also render stub in case parent is no ScBubble
|
||||
fun <H: BaseEventItem.BaseHolder>TimelineMessageLayoutRenderer?.scRenderMessageLayout(messageLayout: TimelineMessageLayout,
|
||||
bubbleDependentView: BubbleDependentView<H>,
|
||||
holder: H) {
|
||||
if (this == null) {
|
||||
renderStubMessageLayout(messageLayout, holder.viewStubContainer)
|
||||
} else {
|
||||
renderMessageLayout(messageLayout, bubbleDependentView, holder)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@ import android.graphics.Paint
|
|||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceManager
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AnonymousReadReceipt
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -22,6 +24,29 @@ class BubbleThemeUtils @Inject constructor(private val context: Context) {
|
|||
const val BUBBLE_STYLE_BOTH = "both"
|
||||
const val BUBBLE_TIME_TOP = "top"
|
||||
const val BUBBLE_TIME_BOTTOM = "bottom"
|
||||
|
||||
fun getVisibleAnonymousReadReceipts(readReceipt: AnonymousReadReceipt?, sentByMe: Boolean): AnonymousReadReceipt {
|
||||
readReceipt ?: return AnonymousReadReceipt.NONE
|
||||
// TODO
|
||||
return if (sentByMe && (/*TODO setting?*/ true || readReceipt == AnonymousReadReceipt.PROCESSING)) {
|
||||
readReceipt
|
||||
} else {
|
||||
AnonymousReadReceipt.NONE
|
||||
}
|
||||
}
|
||||
|
||||
fun anonymousReadReceiptForEvent(event: TimelineEvent): AnonymousReadReceipt {
|
||||
return if (event.root.sendState == SendState.SYNCED || event.root.sendState == SendState.SENT) {
|
||||
/*if (event.readByOther) {
|
||||
AnonymousReadReceipt.READ
|
||||
} else {
|
||||
AnonymousReadReceipt.SENT
|
||||
}*/
|
||||
AnonymousReadReceipt.NONE
|
||||
} else {
|
||||
AnonymousReadReceipt.PROCESSING
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Special case of BUBBLE_STYLE_BOTH, to allow non-bubble items align to the sender either way
|
||||
|
@ -47,15 +72,6 @@ class BubbleThemeUtils @Inject constructor(private val context: Context) {
|
|||
PreferenceManager.getDefaultSharedPreferences(context).edit().putString(BUBBLE_STYLE_KEY, value).apply()
|
||||
}
|
||||
|
||||
fun getVisibleAnonymousReadReceipts(readReceipt: AnonymousReadReceipt, sentByMe: Boolean): AnonymousReadReceipt {
|
||||
// TODO
|
||||
return if (sentByMe && (/*TODO setting*/ true || readReceipt == AnonymousReadReceipt.PROCESSING)) {
|
||||
readReceipt
|
||||
} else {
|
||||
AnonymousReadReceipt.NONE
|
||||
}
|
||||
}
|
||||
|
||||
/* SC-TODO
|
||||
fun drawsActualBubbles(bubbleStyle: String): Boolean {
|
||||
return bubbleStyle == BUBBLE_STYLE_START || bubbleStyle == BUBBLE_STYLE_BOTH
|
||||
|
@ -66,20 +82,6 @@ class BubbleThemeUtils @Inject constructor(private val context: Context) {
|
|||
}
|
||||
*/
|
||||
|
||||
fun guessTextWidth(view: TextView): Float {
|
||||
return guessTextWidth(view, view.text)
|
||||
}
|
||||
|
||||
fun guessTextWidth(view: TextView, text: CharSequence): Float {
|
||||
return guessTextWidth(view.textSize, text);
|
||||
}
|
||||
|
||||
fun guessTextWidth(textSize: Float, text: CharSequence): Float {
|
||||
val paint = Paint()
|
||||
paint.textSize = textSize
|
||||
return paint.measureText(text.toString())
|
||||
}
|
||||
|
||||
fun forceAlwaysShowTimestamps(bubbleStyle: String): Boolean {
|
||||
return isBubbleTimeLocationSettingAllowed(bubbleStyle)
|
||||
}
|
||||
|
@ -96,3 +98,17 @@ class BubbleThemeUtils @Inject constructor(private val context: Context) {
|
|||
return isBubbleTimeLocationSettingAllowed(getBubbleStyle())
|
||||
}
|
||||
}
|
||||
|
||||
fun guessTextWidth(view: TextView): Float {
|
||||
return guessTextWidth(view, view.text)
|
||||
}
|
||||
|
||||
fun guessTextWidth(view: TextView, text: CharSequence): Float {
|
||||
return guessTextWidth(view.textSize, text);
|
||||
}
|
||||
|
||||
fun guessTextWidth(textSize: Float, text: CharSequence): Float {
|
||||
val paint = Paint()
|
||||
paint.textSize = textSize
|
||||
return paint.measureText(text.toString())
|
||||
}
|
||||
|
|
|
@ -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"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/eventBaseView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:addStatesFromChildren="true"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
tools:viewBindingIgnore="true">
|
||||
tools:parentTag="android.widget.RelativeLayout">
|
||||
|
||||
<im.vector.app.core.platform.CheckableView
|
||||
android:id="@+id/messageSelectedBackground"
|
||||
|
@ -116,7 +115,7 @@
|
|||
android:textColor="?vctr_content_primary"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="@sample/matrix.json/data/displayName" />
|
||||
tools:text="@sample/users.json/data/displayName" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bubbleMessageTimeView"
|
||||
|
@ -136,7 +135,10 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/bubbleMessageTimeView"
|
||||
android:layout_margin="0dp"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:addStatesFromChildren="true" />
|
||||
|
||||
<!--
|
||||
|
@ -278,4 +280,4 @@
|
|||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</RelativeLayout>
|
||||
</merge>
|
||||
|
|
Loading…
Reference in New Issue