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">