diff --git a/library/ui-styles/src/main/res/values/stylable_message_bubble.xml b/library/ui-styles/src/main/res/values/stylable_message_bubble.xml
index 1f55d07486..32ed23c613 100644
--- a/library/ui-styles/src/main/res/values/stylable_message_bubble.xml
+++ b/library/ui-styles/src/main/res/values/stylable_message_bubble.xml
@@ -3,6 +3,7 @@
+
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index 241ccb7428..dc5c76725b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -355,12 +355,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
(0 until modelCache.size).forEach { position ->
val event = currentSnapshot[position]
val nextEvent = currentSnapshot.nextOrNull(position)
- val prevEvent = currentSnapshot.prevOrNull(position)
- val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
- timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
- }
// Should be build if not cached or if model should be refreshed
if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState) == false) {
+ val prevEvent = currentSnapshot.prevOrNull(position)
+ val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
+ timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
+ }
val timelineEventsGroup = timelineEventsGroups.getOrNull(event)
val params = TimelineItemFactoryParams(
event = event,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
index 697c307d06..c42d50e924 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt
@@ -389,6 +389,13 @@ class MessageItemFactory @Inject constructor(
allowNonMxcUrls = informationData.sendState.isSending()
)
return MessageImageVideoItem_()
+ .layout(
+ if (informationData.sentByMe) {
+ R.layout.item_timeline_event_bubble_outgoing_base
+ } else {
+ R.layout.item_timeline_event_bubble_incoming_base
+ }
+ )
.attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline)
.imageContentRenderer(imageContentRenderer)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
index b203c23978..b9ef9ca558 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
@@ -57,8 +57,11 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
fun create(params: TimelineItemFactoryParams): MessageInformationData {
val event = params.event
val nextDisplayableEvent = params.nextDisplayableEvent
+ val prevEvent = params.prevEvent
val eventId = event.eventId
val isSentByMe = event.root.senderId == session.myUserId
+ val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId
+ val isLastFromThisSender = prevEvent?.root?.senderId != event.root.senderId
val roomSummary = params.partialState.roomSummary
val date = event.root.localDateTime()
@@ -128,6 +131,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
ReferencesInfoData(verificationState)
},
sentByMe = isSentByMe,
+ isFirstFromThisSender = isFirstFromThisSender,
+ isLastFromThisSender = isLastFromThisSender,
e2eDecoration = e2eDecoration,
sendStateDecoration = sendStateDecoration
)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
index 080b766258..3d9db4e827 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt
@@ -29,6 +29,7 @@ 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.view.MessageViewConfiguration
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
@@ -98,6 +99,10 @@ abstract class AbsBaseMessageItem : BaseEventItem
holder.view.onClick(baseAttributes.itemClickListener)
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
+ (holder.view as? MessageViewConfiguration)?.apply {
+ isFirstFromSender = baseAttributes.informationData.isFirstFromThisSender
+ isLastFromSender = baseAttributes.informationData.isLastFromThisSender
+ }
}
override fun unbind(holder: H) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt
index 3ae91db97c..8e42297bc1 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageImageVideoItem.kt
@@ -29,6 +29,8 @@ import im.vector.app.core.epoxy.onClick
import im.vector.app.core.files.LocalFilesHelper
import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
+import im.vector.app.features.home.room.detail.timeline.view.MessageBubbleView
+import im.vector.app.features.home.room.detail.timeline.view.MessageViewConfiguration
import im.vector.app.features.media.ImageContentRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
@@ -70,6 +72,7 @@ abstract class MessageImageVideoItem : AbsMessageItem(R.id.bubbleView).apply {
+ val bubbleView: ConstraintLayout = findViewById(R.id.bubbleView)
+ bubbleView.apply {
background = createBackgroundDrawable()
outlineProvider = ViewOutlineProvider.BACKGROUND
clipToOutline = true
}
- if (incoming) {
+ if (isIncoming) {
findViewById(R.id.informationBottom).layoutDirection = currentLayoutDirection
findViewById(R.id.bubbleWrapper).layoutDirection = currentLayoutDirection
- findViewById(R.id.bubbleView).layoutDirection = currentLayoutDirection
+ bubbleView.layoutDirection = currentLayoutDirection
findViewById(R.id.messageEndGuideline).updateLayoutParams {
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end)
}
@@ -73,21 +101,37 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri
findViewById(R.id.informationBottom).layoutDirection = oppositeLayoutDirection
findViewById(R.id.bubbleWrapper).layoutDirection = oppositeLayoutDirection
- findViewById(R.id.bubbleView).layoutDirection = currentLayoutDirection
+ bubbleView.layoutDirection = currentLayoutDirection
findViewById(R.id.messageEndGuideline).updateLayoutParams {
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start)
}
}
+ ConstraintSet().apply {
+ clone(bubbleView)
+ clear(R.id.viewStubContainer, ConstraintSet.END)
+ if (displayBorder) {
+ connect(R.id.viewStubContainer, ConstraintSet.END, R.id.messageTimeView, ConstraintSet.START, 0)
+ } else {
+ connect(R.id.viewStubContainer, ConstraintSet.END, R.id.parent, ConstraintSet.END, 0)
+ }
+ applyTo(bubbleView)
+ }
}
private fun createBackgroundDrawable(): Drawable {
- val topCornerFamily = if (isFirst) CornerFamily.ROUNDED else CornerFamily.CUT
- val bottomCornerFamily = if (isLast) CornerFamily.ROUNDED else CornerFamily.CUT
- val topRadius = if (isFirst) cornerRadius else 0f
- val bottomRadius = if (isLast) cornerRadius else 0f
+ val (topCornerFamily, topRadius) = if (isFirstFromSender) {
+ Pair(CornerFamily.ROUNDED, cornerRadius)
+ } else {
+ Pair(CornerFamily.CUT, 0f)
+ }
+ val (bottomCornerFamily, bottomRadius) = if (isLastFromSender) {
+ Pair(CornerFamily.ROUNDED, cornerRadius)
+ } else {
+ Pair(CornerFamily.CUT, 0f)
+ }
val shapeAppearanceModelBuilder = ShapeAppearanceModel().toBuilder()
val backgroundColor: Int
- if (incoming) {
+ if (isIncoming) {
backgroundColor = R.color.bubble_background_incoming
shapeAppearanceModelBuilder
.setTopRightCorner(CornerFamily.ROUNDED, cornerRadius)
@@ -104,7 +148,11 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri
}
val shapeAppearanceModel = shapeAppearanceModelBuilder.build()
val shapeDrawable = MaterialShapeDrawable(shapeAppearanceModel)
- shapeDrawable.fillColor = ContextCompat.getColorStateList(context, backgroundColor)
+ if (displayBorder) {
+ shapeDrawable.fillColor = ContextCompat.getColorStateList(context, backgroundColor)
+ } else {
+ shapeDrawable.fillColor = ContextCompat.getColorStateList(context, android.R.color.transparent)
+ }
return shapeDrawable
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageViewConfiguration.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageViewConfiguration.kt
new file mode 100644
index 0000000000..f441005edf
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageViewConfiguration.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home.room.detail.timeline.view
+
+interface MessageViewConfiguration {
+ var isIncoming: Boolean
+ var isFirstFromSender: Boolean
+ var isLastFromSender: Boolean
+ var displayBorder: Boolean
+}
diff --git a/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml b/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml
index 9e2a5ef3ed..988e2d8bb1 100644
--- a/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml
+++ b/vector/src/main/res/layout/item_timeline_event_media_message_stub.xml
@@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:background="@color/palette_element_green"
tools:viewBindingIgnore="true">