From 9a5934dd33ce66353e63d34206d16ec9a6cdba7f Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Thu, 16 Dec 2021 20:57:05 +0100 Subject: [PATCH 01/36] Bubbles: R&D try to find the best way to provide dynamic layout --- .../res/values/stylable_message_bubble.xml | 8 + .../timeline/factory/MessageItemFactory.kt | 5 + .../timeline/helper/AvatarSizeProvider.kt | 5 +- .../helper/MessageInformationDataFactory.kt | 13 +- .../detail/timeline/item/AbsMessageItem.kt | 44 +++-- .../timeline/item/MessageInformationData.kt | 5 +- .../detail/timeline/item/MessageTextItem.kt | 2 +- .../detail/timeline/view/MessageBubbleView.kt | 58 ++++++ .../drawable/bg_timeline_incoming_message.xml | 5 + .../drawable/bg_timeline_outgoing_message.xml | 5 + .../res/layout/item_timeline_event_base.xml | 60 +------ ...em_timeline_event_bubble_incoming_base.xml | 8 + ...em_timeline_event_bubble_outgoing_base.xml | 8 + .../item_timeline_event_text_message_stub.xml | 2 +- ...em_timeline_event_view_stubs_container.xml | 61 +++++++ .../main/res/layout/view_message_bubble.xml | 170 ++++++++++++++++++ 16 files changed, 368 insertions(+), 91 deletions(-) create mode 100644 library/ui-styles/src/main/res/values/stylable_message_bubble.xml create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt create mode 100644 vector/src/main/res/drawable/bg_timeline_incoming_message.xml create mode 100644 vector/src/main/res/drawable/bg_timeline_outgoing_message.xml create mode 100644 vector/src/main/res/layout/item_timeline_event_bubble_incoming_base.xml create mode 100644 vector/src/main/res/layout/item_timeline_event_bubble_outgoing_base.xml create mode 100644 vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml create mode 100644 vector/src/main/res/layout/view_message_bubble.xml 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 new file mode 100644 index 0000000000..8da2df33e7 --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_message_bubble.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <declare-styleable name="MessageBubble"> + <attr name="incoming_style" format="boolean" /> + </declare-styleable> + +</resources> 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 98deaaf9c3..05973be8be 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 @@ -482,6 +482,11 @@ class MessageItemFactory @Inject constructor( } else { message(linkifiedBody) } + if (informationData.sentByMe) { + layout(R.layout.item_timeline_event_bubble_outgoing_base) + } else { + layout(R.layout.item_timeline_event_bubble_incoming_base) + } } .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) .canUseTextFuture(canUseTextFuture) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt index 5fc5deb407..c2662e7ae6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt @@ -21,10 +21,10 @@ import javax.inject.Inject class AvatarSizeProvider @Inject constructor(private val dimensionConverter: DimensionConverter) { - private val avatarStyle = AvatarStyle.SMALL + private val avatarStyle = AvatarStyle.X_SMALL val leftGuideline: Int by lazy { - dimensionConverter.dpToPx(avatarStyle.avatarSizeDP + 8) + dimensionConverter.dpToPx(avatarStyle.avatarSizeDP) } val avatarSize: Int by lazy { @@ -37,6 +37,7 @@ class AvatarSizeProvider @Inject constructor(private val dimensionConverter: Dim BIG(50), MEDIUM(40), SMALL(30), + X_SMALL(24), NONE(0) } } 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 6385494fe1..eef174c0f3 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,6 +57,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses val event = params.event val nextDisplayableEvent = params.nextDisplayableEvent val eventId = event.eventId + val isSentByMe = event.root.senderId == session.myUserId + val roomSummary = params.partialState.roomSummary val date = event.root.localDateTime() val nextDate = nextDisplayableEvent?.root?.localDateTime() @@ -65,20 +67,18 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ?: false val showInformation = - addDaySeparator || + (addDaySeparator || event.senderInfo.avatarUrl != nextDisplayableEvent?.senderInfo?.avatarUrl || event.senderInfo.disambiguatedDisplayName != nextDisplayableEvent?.senderInfo?.disambiguatedDisplayName || nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) || isNextMessageReceivedMoreThanOneHourAgo || isTileTypeMessage(nextDisplayableEvent) || - nextDisplayableEvent.isEdition() + nextDisplayableEvent.isEdition() ) && !isSentByMe val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) - val roomSummary = params.partialState.roomSummary val e2eDecoration = getE2EDecoration(roomSummary, event) // SendState Decoration - val isSentByMe = event.root.senderId == session.myUserId val sendStateDecoration = if (isSentByMe) { getSendStateDecoration( event = event, @@ -97,8 +97,9 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ageLocalTS = event.root.ageLocalTs, avatarUrl = event.senderInfo.avatarUrl, memberName = event.senderInfo.disambiguatedDisplayName, - showInformation = showInformation, - forceShowTimestamp = vectorPreferences.alwaysShowTimeStamps(), + showAvatar = showInformation, + showDisplayName = showInformation, + showTimestamp = true, orderedReactionList = event.annotations?.reactionsSummary // ?.filter { isSingleEmoji(it.key) } ?.map { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index b53495fdaf..a964af6f73 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -22,7 +22,6 @@ import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.annotation.IdRes -import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import im.vector.app.R @@ -63,38 +62,37 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H> override fun bind(holder: H) { super.bind(holder) - if (attributes.informationData.showInformation) { + if (attributes.informationData.showAvatar) { holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply { height = attributes.avatarSize width = attributes.avatarSize } - holder.avatarImageView.visibility = View.VISIBLE - holder.avatarImageView.onClick(_avatarClickListener) - holder.memberNameView.visibility = View.VISIBLE - holder.memberNameView.onClick(_memberNameClickListener) - holder.timeView.visibility = View.VISIBLE - holder.timeView.text = attributes.informationData.time - holder.memberNameView.text = attributes.informationData.memberName - holder.memberNameView.setTextColor(attributes.getMemberNameColor()) attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView) holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener) - holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener) + holder.avatarImageView.isVisible = true + holder.avatarImageView.onClick(_avatarClickListener) } else { holder.avatarImageView.setOnClickListener(null) - holder.memberNameView.setOnClickListener(null) - holder.avatarImageView.visibility = View.GONE - if (attributes.informationData.forceShowTimestamp) { - holder.memberNameView.isInvisible = true - holder.timeView.isVisible = true - holder.timeView.text = attributes.informationData.time - } else { - holder.memberNameView.isVisible = false - holder.timeView.isVisible = false - } holder.avatarImageView.setOnLongClickListener(null) - holder.memberNameView.setOnLongClickListener(null) + holder.avatarImageView.isVisible = false + } + if (attributes.informationData.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.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 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index 08aa301538..94c6b32b0a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -31,8 +31,9 @@ data class MessageInformationData( val ageLocalTS: Long?, val avatarUrl: String?, val memberName: CharSequence? = null, - val showInformation: Boolean = true, - val forceShowTimestamp: Boolean = false, + val showAvatar: Boolean, + val showDisplayName: Boolean, + val showTimestamp: Boolean, /*List of reactions (emoji,count,isSelected)*/ val orderedReactionList: List<ReactionInfoData>? = null, val pollResponseAggregatedSummary: PollResponseData? = null, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index 747183bce6..d950b7226a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -113,7 +113,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() { previewUrlRetriever?.removeListener(attributes.informationData.eventId, previewUrlViewUpdater) } - override fun getViewType() = STUB_ID + override fun getViewType() = STUB_ID + layout class Holder : AbsMessageItem.Holder(STUB_ID) { val messageView by bind<AppCompatTextView>(R.id.messageTextView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt new file mode 100644 index 0000000000..66fb5cd998 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2021 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 + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.RelativeLayout +import androidx.core.content.withStyledAttributes +import im.vector.app.R + +class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, + defStyleAttr: Int = 0) : RelativeLayout(context, attrs, defStyleAttr) { + + var incoming: Boolean = false + + init { + inflate(context, R.layout.view_message_bubble, this) + context.withStyledAttributes(attrs, R.styleable.MessageBubble) { + incoming = getBoolean(R.styleable.MessageBubble_incoming_style, false) + } + } + + override fun onFinishInflate() { + super.onFinishInflate() + val currentLayoutDirection = layoutDirection + if (incoming) { + findViewById<View>(R.id.informationBottom).layoutDirection = currentLayoutDirection + findViewById<View>(R.id.bubbleWrapper).layoutDirection = currentLayoutDirection + findViewById<View>(R.id.bubbleView).layoutDirection = currentLayoutDirection + findViewById<RelativeLayout>(R.id.bubbleView).setBackgroundResource(R.drawable.bg_timeline_incoming_message) + } else { + val oppositeLayoutDirection = if (currentLayoutDirection == View.LAYOUT_DIRECTION_LTR) { + View.LAYOUT_DIRECTION_RTL + } else { + View.LAYOUT_DIRECTION_LTR + } + findViewById<View>(R.id.informationBottom).layoutDirection = oppositeLayoutDirection + findViewById<View>(R.id.bubbleWrapper).layoutDirection = oppositeLayoutDirection + findViewById<View>(R.id.bubbleView).layoutDirection = currentLayoutDirection + findViewById<RelativeLayout>(R.id.bubbleView).setBackgroundResource(R.drawable.bg_timeline_outgoing_message) + } + } +} diff --git a/vector/src/main/res/drawable/bg_timeline_incoming_message.xml b/vector/src/main/res/drawable/bg_timeline_incoming_message.xml new file mode 100644 index 0000000000..ad4a28a770 --- /dev/null +++ b/vector/src/main/res/drawable/bg_timeline_incoming_message.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="?attr/vctr_system" /> + <corners android:radius="12dp"/> +</shape> \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_timeline_outgoing_message.xml b/vector/src/main/res/drawable/bg_timeline_outgoing_message.xml new file mode 100644 index 0000000000..1ab85d2352 --- /dev/null +++ b/vector/src/main/res/drawable/bg_timeline_outgoing_message.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="#0F0DBD8B" /> + <corners android:radius="12dp"/> +</shape> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 3316a651d7..debf578519 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -76,68 +76,16 @@ android:visibility="gone" tools:visibility="visible" /> - <FrameLayout + <include android:id="@+id/viewStubContainer" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" + layout="@layout/item_timeline_event_view_stubs_container" android:layout_below="@id/messageMemberNameView" android:layout_marginEnd="8dp" android:layout_toStartOf="@id/messageSendStateImageView" android:layout_toEndOf="@id/messageStartGuideline" - android:addStatesFromChildren="true"> - - <ViewStub - android:id="@+id/messageContentTextStub" - style="@style/TimelineContentStubBaseParams" - android:layout_height="wrap_content" - android:layout="@layout/item_timeline_event_text_message_stub" - tools:visibility="visible" /> - - <ViewStub - android:id="@+id/messageContentCodeBlockStub" - style="@style/TimelineContentStubBaseParams" - android:layout_height="wrap_content" - android:layout="@layout/item_timeline_event_code_block_stub" - tools:visibility="visible" /> - - <ViewStub - android:id="@+id/messageContentMediaStub" - style="@style/TimelineContentStubBaseParams" - android:layout_height="wrap_content" - android:inflatedId="@+id/messageContentMedia" - android:layout="@layout/item_timeline_event_media_message_stub" /> - - <ViewStub - android:id="@+id/messageContentFileStub" - style="@style/TimelineContentStubBaseParams" - android:layout_height="wrap_content" - android:layout="@layout/item_timeline_event_file_stub" /> - - <ViewStub - android:id="@+id/messageContentRedactedStub" - style="@style/TimelineContentStubBaseParams" - android:layout_height="wrap_content" - android:layout="@layout/item_timeline_event_redacted_stub" /> - - <ViewStub - android:id="@+id/messagePollStub" - style="@style/TimelineContentStubBaseParams" - android:layout_height="wrap_content" - android:layout="@layout/item_timeline_event_poll_stub" /> - - <ViewStub - android:id="@+id/messageOptionsStub" - style="@style/TimelineContentStubBaseParams" - android:layout_height="wrap_content" - android:layout="@layout/item_timeline_event_option_buttons_stub" /> - - <ViewStub - android:id="@+id/messageContentVoiceStub" - style="@style/TimelineContentStubBaseParams" - android:layout="@layout/item_timeline_event_voice_stub" - tools:visibility="visible" /> - - </FrameLayout> + android:addStatesFromChildren="true"/> <im.vector.app.core.ui.views.SendStateImageView android:id="@+id/messageSendStateImageView" diff --git a/vector/src/main/res/layout/item_timeline_event_bubble_incoming_base.xml b/vector/src/main/res/layout/item_timeline_event_bubble_incoming_base.xml new file mode 100644 index 0000000000..ac8e3aad48 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_bubble_incoming_base.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<im.vector.app.features.home.room.detail.timeline.view.MessageBubbleView 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" + android:background="?attr/selectableItemBackground" + app:incoming_style="true" /> diff --git a/vector/src/main/res/layout/item_timeline_event_bubble_outgoing_base.xml b/vector/src/main/res/layout/item_timeline_event_bubble_outgoing_base.xml new file mode 100644 index 0000000000..42bf1b5f7a --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_bubble_outgoing_base.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<im.vector.app.features.home.room.detail.timeline.view.MessageBubbleView 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" + android:background="?attr/selectableItemBackground" + app:incoming_style="false" /> diff --git a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml index 3f9feb93af..4bb612fedf 100644 --- a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml @@ -23,6 +23,6 @@ android:layout_marginBottom="4dp" android:foreground="?attr/selectableItemBackground" android:visibility="gone" - tools:visibility="visible" /> + tools:visibility="gone" /> </LinearLayout> diff --git a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml new file mode 100644 index 0000000000..04613c665f --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:addStatesFromChildren="true"> + + <ViewStub + android:id="@+id/messageContentTextStub" + style="@style/TimelineContentStubBaseParams" + android:layout_height="wrap_content" + android:layout="@layout/item_timeline_event_text_message_stub" + tools:visibility="visible" /> + + <ViewStub + android:id="@+id/messageContentCodeBlockStub" + style="@style/TimelineContentStubBaseParams" + android:layout_height="wrap_content" + android:layout="@layout/item_timeline_event_code_block_stub" + tools:visibility="visible" /> + + <ViewStub + android:id="@+id/messageContentMediaStub" + style="@style/TimelineContentStubBaseParams" + android:layout_height="wrap_content" + android:inflatedId="@+id/messageContentMedia" + android:layout="@layout/item_timeline_event_media_message_stub" /> + + <ViewStub + android:id="@+id/messageContentFileStub" + style="@style/TimelineContentStubBaseParams" + android:layout_height="wrap_content" + android:layout="@layout/item_timeline_event_file_stub" /> + + <ViewStub + android:id="@+id/messageContentRedactedStub" + style="@style/TimelineContentStubBaseParams" + android:layout_height="wrap_content" + android:layout="@layout/item_timeline_event_redacted_stub" /> + + <ViewStub + android:id="@+id/messagePollStub" + style="@style/TimelineContentStubBaseParams" + android:layout_height="wrap_content" + android:layout="@layout/item_timeline_event_poll_stub" /> + + <ViewStub + android:id="@+id/messageOptionsStub" + style="@style/TimelineContentStubBaseParams" + android:layout_height="wrap_content" + android:layout="@layout/item_timeline_event_option_buttons_stub" /> + + <ViewStub + android:id="@+id/messageContentVoiceStub" + style="@style/TimelineContentStubBaseParams" + android:layout="@layout/item_timeline_event_voice_stub" + tools:visibility="visible" /> + +</FrameLayout> + + diff --git a/vector/src/main/res/layout/view_message_bubble.xml b/vector/src/main/res/layout/view_message_bubble.xml new file mode 100644 index 0000000000..36c5592f07 --- /dev/null +++ b/vector/src/main/res/layout/view_message_bubble.xml @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="utf-8"?> +<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:layout_width="match_parent" + android:layout_height="wrap_content" + tools:parentTag="android.widget.RelativeLayout" + tools:viewBindingIgnore="true"> + + <im.vector.app.core.platform.CheckableView + android:id="@+id/messageSelectedBackground" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_alignBottom="@id/informationBottom" + android:layout_alignParentTop="true" + android:background="@drawable/highlighted_message_background" /> + + <ImageView + android:id="@+id/messageAvatarImageView" + android:layout_width="44dp" + android:layout_height="44dp" + android:layout_marginStart="8dp" + android:layout_marginTop="4dp" + android:contentDescription="@string/avatar" + tools:src="@sample/user_round_avatars" /> + + <TextView + android:id="@+id/messageMemberNameView" + style="@style/Widget.Vector.TextView.Subtitle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_alignParentEnd="true" + android:layout_marginStart="8dp" + android:layout_marginEnd="4dp" + android:layout_toEndOf="@id/messageStartGuideline" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?vctr_content_primary" + android:textStyle="bold" + tools:text="@sample/users.json/data/displayName" /> + + <View + android:id="@+id/messageStartGuideline" + android:layout_width="0dp" + android:layout_height="0dp" + tools:layout_marginStart="52dp" /> + + <Space + android:id="@+id/decorationSpace" + android:layout_width="4dp" + android:layout_height="8dp" + android:layout_toEndOf="@id/messageStartGuideline" /> + + <im.vector.app.core.ui.views.ShieldImageView + android:id="@+id/messageE2EDecoration" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_alignTop="@id/bubbleWrapper" + android:layout_alignEnd="@id/decorationSpace" + android:layout_marginTop="7dp" + android:visibility="gone" + tools:visibility="visible" /> + + <FrameLayout + android:id="@+id/bubbleWrapper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/messageMemberNameView" + android:layout_toStartOf="@id/messageSendStateImageView" + android:layout_toEndOf="@id/messageStartGuideline" + android:addStatesFromChildren="true" + android:clipChildren="false" + android:clipToPadding="false"> + + <RelativeLayout + android:id="@+id/bubbleView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginVertical="4dp" + android:layout_marginStart="0dp" + android:layout_marginEnd="0dp" + android:addStatesFromChildren="true" + android:background="@drawable/bg_timeline_incoming_message" + android:clipChildren="false" + android:clipToPadding="false" + tools:ignore="UselessParent"> + + <include + android:id="@+id/viewStubContainer" + layout="@layout/item_timeline_event_view_stubs_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:addStatesFromChildren="true" /> + + <TextView + android:id="@+id/messageTimeView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignBottom="@id/viewStubContainer" + android:layout_marginEnd="8dp" + android:layout_marginBottom="4dp" + android:layout_toEndOf="@id/viewStubContainer" + android:textColor="?vctr_content_tertiary" + android:textSize="10sp" + tools:text="@tools:sample/date/hhmm" /> + + </RelativeLayout> + </FrameLayout> + + <im.vector.app.core.ui.views.SendStateImageView + android:id="@+id/messageSendStateImageView" + android:layout_width="@dimen/item_event_message_state_size" + android:layout_height="@dimen/item_event_message_state_size" + android:layout_alignBottom="@id/bubbleWrapper" + android:layout_alignParentEnd="true" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="4dp" + android:contentDescription="@string/event_status_a11y_sending" + android:src="@drawable/ic_sending_message" + android:visibility="invisible" + tools:tint="?vctr_content_tertiary" + tools:visibility="visible" /> + + <ProgressBar + android:id="@+id/eventSendingIndicator" + android:layout_width="@dimen/item_event_message_state_size" + android:layout_height="@dimen/item_event_message_state_size" + android:layout_alignBottom="@id/bubbleWrapper" + android:layout_alignParentEnd="true" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="4dp" + android:indeterminateTint="?vctr_content_secondary" + android:visibility="gone" + app:tint="?vctr_content_tertiary" + tools:ignore="MissingPrefix" + tools:visibility="visible" /> + + <LinearLayout + android:id="@+id/informationBottom" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/bubbleWrapper" + android:layout_toEndOf="@id/messageStartGuideline" + android:addStatesFromChildren="true" + android:orientation="vertical"> + + <com.google.android.flexbox.FlexboxLayout + android:id="@+id/reactionsContainer" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginBottom="4dp" + app:dividerDrawable="@drawable/reaction_divider" + app:flexWrap="wrap" + app:showDivider="middle" + tools:background="#F0E0F0" + tools:layout_height="40dp"> + + <!-- ReactionButtons will be added here in the code --> + <!--im.vector.app.features.reactions.widget.ReactionButton + android:layout_width="wrap_content" + android:layout_height="wrap_content" /--> + + </com.google.android.flexbox.FlexboxLayout> + </LinearLayout> + +</merge> \ No newline at end of file From bde1df03224ac8cb5422467cb8347da973b687c2 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Wed, 5 Jan 2022 11:00:12 +0100 Subject: [PATCH 02/36] Bubbles: continue R&D on UI --- .../ui-styles/src/main/res/values/colors.xml | 4 ++ .../ui-styles/src/main/res/values/dimens.xml | 4 ++ .../res/values/stylable_message_bubble.xml | 2 + .../app/features/home/AvatarRenderer.kt | 1 + .../timeline/helper/AvatarSizeProvider.kt | 4 +- .../detail/timeline/view/MessageBubbleView.kt | 56 ++++++++++++++++++- .../main/res/drawable/bg_avatar_border.xml | 12 ++++ .../drawable/bg_timeline_incoming_message.xml | 1 - .../drawable/bg_timeline_outgoing_message.xml | 1 - .../main/res/layout/view_message_bubble.xml | 43 +++++++++----- 10 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 vector/src/main/res/drawable/bg_avatar_border.xml diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index 9df2794a1a..e3ec542c89 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -13,6 +13,9 @@ <color name="button_bot_background_color">#14368BD6</color> <color name="button_bot_enabled_text_color">@color/palette_azure</color> + <color name="bubble_background_outgoing">#0F0DBD8B</color> + <color name="bubble_background_incoming">@color/element_system_light</color> + <!-- Notification (do not depends on theme) --> <color name="notification_accent_color">@color/palette_azure</color> <color name="key_share_req_accent_color">@color/palette_melon</color> @@ -137,4 +140,5 @@ <attr name="vctr_presence_indicator_offline" format="color" /> <color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</color> <color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color> + </resources> diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index 9fbf8958da..6eff9295d0 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -47,4 +47,8 @@ <dimen name="composer_min_height">56dp</dimen> <dimen name="composer_attachment_size">52dp</dimen> <dimen name="composer_attachment_margin">1dp</dimen> + + <dimen name="chat_bubble_margin_start">28dp</dimen> + <dimen name="chat_bubble_margin_end">62dp</dimen> + </resources> \ No newline at end of file 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 8da2df33e7..1f55d07486 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,8 @@ <declare-styleable name="MessageBubble"> <attr name="incoming_style" format="boolean" /> + <attr name="is_first" format="boolean" /> + <attr name="is_last" format="boolean" /> </declare-styleable> </resources> diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index 2ee3233637..be689eb27a 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -145,6 +145,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active } else -> { it.apply(RequestOptions.circleCropTransform()) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt index c2662e7ae6..00b02c2cf0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt @@ -24,7 +24,7 @@ class AvatarSizeProvider @Inject constructor(private val dimensionConverter: Dim private val avatarStyle = AvatarStyle.X_SMALL val leftGuideline: Int by lazy { - dimensionConverter.dpToPx(avatarStyle.avatarSizeDP) + dimensionConverter.dpToPx(avatarStyle.avatarSizeDP + 4) } val avatarSize: Int by lazy { @@ -37,7 +37,7 @@ class AvatarSizeProvider @Inject constructor(private val dimensionConverter: Dim BIG(50), MEDIUM(40), SMALL(30), - X_SMALL(24), + X_SMALL(28), NONE(0) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 66fb5cd998..9ad7eb097c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -17,42 +17,94 @@ package im.vector.app.features.home.room.detail.timeline.view import android.content.Context +import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.View +import android.view.ViewGroup +import android.view.ViewOutlineProvider import android.widget.RelativeLayout +import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes +import androidx.core.view.updateLayoutParams +import com.google.android.material.shape.CornerFamily +import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.shape.ShapeAppearanceModel import im.vector.app.R +import im.vector.app.core.utils.DimensionConverter class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : RelativeLayout(context, attrs, defStyleAttr) { var incoming: Boolean = false + var isFirst: Boolean = false + var isLast: Boolean = false + var cornerRadius = DimensionConverter(resources).dpToPx(12).toFloat() init { inflate(context, R.layout.view_message_bubble, this) context.withStyledAttributes(attrs, R.styleable.MessageBubble) { incoming = getBoolean(R.styleable.MessageBubble_incoming_style, false) + isFirst = getBoolean(R.styleable.MessageBubble_is_first, false) + isLast = getBoolean(R.styleable.MessageBubble_is_last, false) } } override fun onFinishInflate() { super.onFinishInflate() val currentLayoutDirection = layoutDirection + findViewById<ViewGroup>(R.id.bubbleView).apply { + background = createBackgroundDrawable() + outlineProvider = ViewOutlineProvider.BACKGROUND + clipToOutline = true + } if (incoming) { findViewById<View>(R.id.informationBottom).layoutDirection = currentLayoutDirection findViewById<View>(R.id.bubbleWrapper).layoutDirection = currentLayoutDirection findViewById<View>(R.id.bubbleView).layoutDirection = currentLayoutDirection - findViewById<RelativeLayout>(R.id.bubbleView).setBackgroundResource(R.drawable.bg_timeline_incoming_message) + findViewById<View>(R.id.messageEndGuideline).updateLayoutParams<LayoutParams> { + marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end) + } } else { val oppositeLayoutDirection = if (currentLayoutDirection == View.LAYOUT_DIRECTION_LTR) { View.LAYOUT_DIRECTION_RTL } else { View.LAYOUT_DIRECTION_LTR } + findViewById<View>(R.id.informationBottom).layoutDirection = oppositeLayoutDirection findViewById<View>(R.id.bubbleWrapper).layoutDirection = oppositeLayoutDirection findViewById<View>(R.id.bubbleView).layoutDirection = currentLayoutDirection - findViewById<RelativeLayout>(R.id.bubbleView).setBackgroundResource(R.drawable.bg_timeline_outgoing_message) + findViewById<View>(R.id.messageEndGuideline).updateLayoutParams<LayoutParams> { + marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start) + } } } + + 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 shapeAppearanceModelBuilder = ShapeAppearanceModel().toBuilder() + val backgroundColor: Int + if (incoming) { + backgroundColor = R.color.bubble_background_incoming + shapeAppearanceModelBuilder + .setTopRightCorner(CornerFamily.ROUNDED, cornerRadius) + .setBottomRightCorner(CornerFamily.ROUNDED, cornerRadius) + .setTopLeftCorner(topCornerFamily, topRadius) + .setBottomLeftCorner(bottomCornerFamily, bottomRadius) + } else { + backgroundColor = R.color.bubble_background_outgoing + shapeAppearanceModelBuilder + .setTopLeftCorner(CornerFamily.ROUNDED, cornerRadius) + .setBottomLeftCorner(CornerFamily.ROUNDED, cornerRadius) + .setTopRightCorner(topCornerFamily, topRadius) + .setBottomRightCorner(bottomCornerFamily, bottomRadius) + } + val shapeAppearanceModel = shapeAppearanceModelBuilder.build() + val shapeDrawable = MaterialShapeDrawable(shapeAppearanceModel) + shapeDrawable.fillColor = ContextCompat.getColorStateList(context, backgroundColor) + return shapeDrawable + } } diff --git a/vector/src/main/res/drawable/bg_avatar_border.xml b/vector/src/main/res/drawable/bg_avatar_border.xml new file mode 100644 index 0000000000..e22731c1a3 --- /dev/null +++ b/vector/src/main/res/drawable/bg_avatar_border.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="oval"> + + <solid android:color="@android:color/transparent"/> + + <stroke + android:width="2dp" + android:color="?android:colorBackground"/> + +</shape> \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_timeline_incoming_message.xml b/vector/src/main/res/drawable/bg_timeline_incoming_message.xml index ad4a28a770..2cbca33702 100644 --- a/vector/src/main/res/drawable/bg_timeline_incoming_message.xml +++ b/vector/src/main/res/drawable/bg_timeline_incoming_message.xml @@ -1,5 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="?attr/vctr_system" /> - <corners android:radius="12dp"/> </shape> \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_timeline_outgoing_message.xml b/vector/src/main/res/drawable/bg_timeline_outgoing_message.xml index 1ab85d2352..0f75705a77 100644 --- a/vector/src/main/res/drawable/bg_timeline_outgoing_message.xml +++ b/vector/src/main/res/drawable/bg_timeline_outgoing_message.xml @@ -1,5 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="#0F0DBD8B" /> - <corners android:radius="12dp"/> </shape> \ No newline at end of file diff --git a/vector/src/main/res/layout/view_message_bubble.xml b/vector/src/main/res/layout/view_message_bubble.xml index 36c5592f07..12f16910f1 100644 --- a/vector/src/main/res/layout/view_message_bubble.xml +++ b/vector/src/main/res/layout/view_message_bubble.xml @@ -19,9 +19,12 @@ android:id="@+id/messageAvatarImageView" android:layout_width="44dp" android:layout_height="44dp" - android:layout_marginStart="8dp" + android:layout_marginStart="12dp" android:layout_marginTop="4dp" + android:padding="2dp" + android:background="@drawable/bg_avatar_border" android:contentDescription="@string/avatar" + android:elevation="2dp" tools:src="@sample/user_round_avatars" /> <TextView @@ -31,7 +34,7 @@ android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_alignParentEnd="true" - android:layout_marginStart="8dp" + android:layout_marginStart="12dp" android:layout_marginEnd="4dp" android:layout_toEndOf="@id/messageStartGuideline" android:ellipsize="end" @@ -46,6 +49,13 @@ android:layout_height="0dp" tools:layout_marginStart="52dp" /> + <View + android:id="@+id/messageEndGuideline" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_alignParentEnd="true" + android:layout_marginEnd="64dp" /> + <Space android:id="@+id/decorationSpace" android:layout_width="4dp" @@ -59,6 +69,7 @@ android:layout_alignTop="@id/bubbleWrapper" android:layout_alignEnd="@id/decorationSpace" android:layout_marginTop="7dp" + android:elevation="2dp" android:visibility="gone" tools:visibility="visible" /> @@ -67,45 +78,49 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/messageMemberNameView" - android:layout_toStartOf="@id/messageSendStateImageView" + android:layout_toStartOf="@id/messageEndGuideline" android:layout_toEndOf="@id/messageStartGuideline" android:addStatesFromChildren="true" android:clipChildren="false" android:clipToPadding="false"> - <RelativeLayout + <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/bubbleView" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginVertical="4dp" android:layout_marginStart="0dp" android:layout_marginEnd="0dp" android:addStatesFromChildren="true" android:background="@drawable/bg_timeline_incoming_message" - android:clipChildren="false" - android:clipToPadding="false" - tools:ignore="UselessParent"> + android:paddingStart="4dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> <include android:id="@+id/viewStubContainer" layout="@layout/item_timeline_event_view_stubs_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:addStatesFromChildren="true" /> + android:addStatesFromChildren="true" + app:layout_constrainedWidth="true" + app:layout_constraintEnd_toStartOf="@id/messageTimeView" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_max="300dp" /> <TextView android:id="@+id/messageTimeView" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignBottom="@id/viewStubContainer" android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" - android:layout_toEndOf="@id/viewStubContainer" android:textColor="?vctr_content_tertiary" android:textSize="10sp" + app:layout_constraintBottom_toBottomOf="@id/viewStubContainer" + app:layout_constraintEnd_toEndOf="parent" tools:text="@tools:sample/date/hhmm" /> - </RelativeLayout> + </androidx.constraintlayout.widget.ConstraintLayout> </FrameLayout> <im.vector.app.core.ui.views.SendStateImageView @@ -143,6 +158,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/bubbleWrapper" + android:layout_marginTop="2dp" + android:layout_marginBottom="2dp" + android:layout_toStartOf="@id/messageEndGuideline" android:layout_toEndOf="@id/messageStartGuideline" android:addStatesFromChildren="true" android:orientation="vertical"> @@ -151,7 +169,6 @@ android:id="@+id/reactionsContainer" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="8dp" android:layout_marginBottom="4dp" app:dividerDrawable="@drawable/reaction_divider" app:flexWrap="wrap" From ad63d3de1c57cb8d9bf83741c7c59771fb7fc514 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Thu, 6 Jan 2022 19:07:28 +0100 Subject: [PATCH 03/36] Bubbles: still R&D. Not sure how to handle every event types. --- .../res/values/stylable_message_bubble.xml | 1 + .../timeline/TimelineEventController.kt | 8 +- .../timeline/factory/MessageItemFactory.kt | 7 ++ .../helper/MessageInformationDataFactory.kt | 5 ++ .../timeline/item/AbsBaseMessageItem.kt | 5 ++ .../timeline/item/MessageImageVideoItem.kt | 3 + .../timeline/item/MessageInformationData.kt | 4 +- .../detail/timeline/view/MessageBubbleView.kt | 86 +++++++++++++++---- .../timeline/view/MessageViewConfiguration.kt | 24 ++++++ ...item_timeline_event_media_message_stub.xml | 1 + .../main/res/layout/view_message_bubble.xml | 2 +- 11 files changed, 121 insertions(+), 25 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageViewConfiguration.kt 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 @@ <declare-styleable name="MessageBubble"> <attr name="incoming_style" format="boolean" /> + <attr name="show_background" format="boolean" /> <attr name="is_first" format="boolean" /> <attr name="is_last" format="boolean" /> </declare-styleable> 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<H : AbsBaseMessageItem.Holder> : 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<MessageImageVideoItem.Hold holder.mediaContentView.onClick(attributes.itemClickListener) holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener) holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE + (holder.view as? MessageViewConfiguration)?.displayBorder = false } override fun unbind(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index b105bfd6be..76fc9a5eff 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -42,7 +42,9 @@ data class MessageInformationData( val referencesInfoData: ReferencesInfoData? = null, val sentByMe: Boolean, val e2eDecoration: E2EDecoration = E2EDecoration.NONE, - val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE + val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE, + val isFirstFromThisSender: Boolean = false, + val isLastFromThisSender: Boolean = false ) : Parcelable { val matrixItem: MatrixItem diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 9ad7eb097c..937d78ebb6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -20,9 +20,10 @@ import android.content.Context import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.View -import android.view.ViewGroup import android.view.ViewOutlineProvider import android.widget.RelativeLayout +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes import androidx.core.view.updateLayoutParams @@ -33,34 +34,61 @@ import im.vector.app.R import im.vector.app.core.utils.DimensionConverter class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0) : RelativeLayout(context, attrs, defStyleAttr) { + defStyleAttr: Int = 0) + : RelativeLayout(context, attrs, defStyleAttr), MessageViewConfiguration { - var incoming: Boolean = false - var isFirst: Boolean = false - var isLast: Boolean = false - var cornerRadius = DimensionConverter(resources).dpToPx(12).toFloat() + override var isIncoming: Boolean = false + set(value) { + field = value + render() + } + + override var isFirstFromSender: Boolean = false + set(value) { + field = value + render() + } + override var isLastFromSender: Boolean = false + set(value) { + field = value + render() + } + + override var displayBorder: Boolean = true + set(value) { + field = value + render() + } + + private val cornerRadius = DimensionConverter(resources).dpToPx(12).toFloat() init { inflate(context, R.layout.view_message_bubble, this) context.withStyledAttributes(attrs, R.styleable.MessageBubble) { - incoming = getBoolean(R.styleable.MessageBubble_incoming_style, false) - isFirst = getBoolean(R.styleable.MessageBubble_is_first, false) - isLast = getBoolean(R.styleable.MessageBubble_is_last, false) + isIncoming = getBoolean(R.styleable.MessageBubble_incoming_style, false) + displayBorder = getBoolean(R.styleable.MessageBubble_show_background, true) + isFirstFromSender = getBoolean(R.styleable.MessageBubble_is_first, false) + isLastFromSender = getBoolean(R.styleable.MessageBubble_is_last, false) } } override fun onFinishInflate() { super.onFinishInflate() + render() + } + + private fun render() { val currentLayoutDirection = layoutDirection - findViewById<ViewGroup>(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<View>(R.id.informationBottom).layoutDirection = currentLayoutDirection findViewById<View>(R.id.bubbleWrapper).layoutDirection = currentLayoutDirection - findViewById<View>(R.id.bubbleView).layoutDirection = currentLayoutDirection + bubbleView.layoutDirection = currentLayoutDirection findViewById<View>(R.id.messageEndGuideline).updateLayoutParams<LayoutParams> { marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end) } @@ -73,21 +101,37 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri findViewById<View>(R.id.informationBottom).layoutDirection = oppositeLayoutDirection findViewById<View>(R.id.bubbleWrapper).layoutDirection = oppositeLayoutDirection - findViewById<View>(R.id.bubbleView).layoutDirection = currentLayoutDirection + bubbleView.layoutDirection = currentLayoutDirection findViewById<View>(R.id.messageEndGuideline).updateLayoutParams<LayoutParams> { 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"> <ImageView diff --git a/vector/src/main/res/layout/view_message_bubble.xml b/vector/src/main/res/layout/view_message_bubble.xml index 12f16910f1..8e9a95222a 100644 --- a/vector/src/main/res/layout/view_message_bubble.xml +++ b/vector/src/main/res/layout/view_message_bubble.xml @@ -103,7 +103,7 @@ android:layout_height="wrap_content" android:addStatesFromChildren="true" app:layout_constrainedWidth="true" - app:layout_constraintEnd_toStartOf="@id/messageTimeView" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintWidth_max="300dp" /> From f7c9b36cef33a037c0237f0c37f817ccdb542042 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Tue, 11 Jan 2022 11:57:35 +0100 Subject: [PATCH 04/36] Bubbles: continue exploration --- .../main/res/values/stylable_message_bubble.xml | 2 +- .../detail/timeline/TimelineEventController.kt | 4 ++++ .../timeline/factory/TimelineItemFactoryParams.kt | 1 + .../helper/MessageInformationDataFactory.kt | 10 ++++++---- .../detail/timeline/item/MessageImageVideoItem.kt | 3 +-- .../detail/timeline/view/MessageBubbleView.kt | 15 ++++++--------- .../timeline/view/MessageViewConfiguration.kt | 2 +- .../item_timeline_event_media_message_stub.xml | 2 -- 8 files changed, 20 insertions(+), 19 deletions(-) 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 32ed23c613..f7a877e3ed 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,7 +3,7 @@ <declare-styleable name="MessageBubble"> <attr name="incoming_style" format="boolean" /> - <attr name="show_background" format="boolean" /> + <attr name="show_time_overlay" format="boolean" /> <attr name="is_first" format="boolean" /> <attr name="is_last" format="boolean" /> </declare-styleable> 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 dc5c76725b..460c332812 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 @@ -358,6 +358,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // 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 prevDisplayableEvent = currentSnapshot.subList(0, position).lastOrNull { + timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId) + } val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull { timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId) } @@ -365,6 +368,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec val params = TimelineItemFactoryParams( event = event, prevEvent = prevEvent, + prevDisplayableEvent = prevDisplayableEvent, nextEvent = nextEvent, nextDisplayableEvent = nextDisplayableEvent, partialState = partialState, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt index cdfedb2925..fad558344c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent data class TimelineItemFactoryParams( val event: TimelineEvent, val prevEvent: TimelineEvent? = null, + val prevDisplayableEvent: TimelineEvent? = null, val nextEvent: TimelineEvent? = null, val nextDisplayableEvent: TimelineEvent? = null, val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(), 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 b9ef9ca558..b9ea78e0db 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,19 +57,21 @@ 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 prevDisplayableEvent = params.prevDisplayableEvent 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() val nextDate = nextDisplayableEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() + val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60)) ?: false + val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator + val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId || prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() + val showInformation = (addDaySeparator || event.senderInfo.avatarUrl != nextDisplayableEvent?.senderInfo?.avatarUrl || @@ -77,7 +79,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) || isNextMessageReceivedMoreThanOneHourAgo || isTileTypeMessage(nextDisplayableEvent) || - nextDisplayableEvent.isEdition() ) && !isSentByMe + nextDisplayableEvent.isEdition()) && !isSentByMe val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) val e2eDecoration = getE2EDecoration(roomSummary, event) 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 8e42297bc1..e865354747 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,7 +29,6 @@ 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 @@ -72,7 +71,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold holder.mediaContentView.onClick(attributes.itemClickListener) holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener) holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE - (holder.view as? MessageViewConfiguration)?.displayBorder = false + (holder.view as? MessageViewConfiguration)?.showTimeAsOverlay = false } override fun unbind(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 937d78ebb6..5bb732bdde 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -54,7 +54,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri render() } - override var displayBorder: Boolean = true + override var showTimeAsOverlay: Boolean = true set(value) { field = value render() @@ -66,7 +66,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri inflate(context, R.layout.view_message_bubble, this) context.withStyledAttributes(attrs, R.styleable.MessageBubble) { isIncoming = getBoolean(R.styleable.MessageBubble_incoming_style, false) - displayBorder = getBoolean(R.styleable.MessageBubble_show_background, true) + showTimeAsOverlay = getBoolean(R.styleable.MessageBubble_show_time_overlay, true) isFirstFromSender = getBoolean(R.styleable.MessageBubble_is_first, false) isLastFromSender = getBoolean(R.styleable.MessageBubble_is_last, false) } @@ -109,13 +109,14 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri ConstraintSet().apply { clone(bubbleView) clear(R.id.viewStubContainer, ConstraintSet.END) - if (displayBorder) { + if (showTimeAsOverlay) { 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 { @@ -147,12 +148,8 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri .setBottomRightCorner(bottomCornerFamily, bottomRadius) } val shapeAppearanceModel = shapeAppearanceModelBuilder.build() - val shapeDrawable = MaterialShapeDrawable(shapeAppearanceModel) - if (displayBorder) { - shapeDrawable.fillColor = ContextCompat.getColorStateList(context, backgroundColor) - } else { - shapeDrawable.fillColor = ContextCompat.getColorStateList(context, android.R.color.transparent) + return MaterialShapeDrawable(shapeAppearanceModel).apply { + fillColor = ContextCompat.getColorStateList(context, backgroundColor) } - 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 index f441005edf..bfb5884674 100644 --- 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 @@ -20,5 +20,5 @@ interface MessageViewConfiguration { var isIncoming: Boolean var isFirstFromSender: Boolean var isLastFromSender: Boolean - var displayBorder: Boolean + var showTimeAsOverlay: 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 988e2d8bb1..4fe37830c2 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,14 +4,12 @@ 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"> <ImageView android:id="@+id/messageThumbnailView" android:layout_width="375dp" android:layout_height="0dp" - android:layout_marginEnd="32dp" android:contentDescription="@string/a11y_image" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0" From af542a8243ff562791f8886cb438159bd314a253 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Tue, 11 Jan 2022 15:38:58 +0100 Subject: [PATCH 05/36] Bubbles: start adding "theming" mechanism --- .../timeline/factory/EncryptedItemFactory.kt | 1 + .../timeline/factory/MessageItemFactory.kt | 19 +--- .../timeline/helper/AvatarSizeProvider.kt | 26 ++++-- .../helper/MessageInformationDataFactory.kt | 24 ++--- .../detail/timeline/item/AbsMessageItem.kt | 6 +- .../timeline/item/MessageInformationData.kt | 5 +- .../timeline/style/TimelineLayoutSettings.kt | 22 +++++ .../style/TimelineLayoutSettingsProvider.kt | 26 ++++++ .../timeline/style/TimelineMessageLayout.kt | 44 ++++++++++ .../style/TimelineMessageLayoutFactory.kt | 88 +++++++++++++++++++ 10 files changed, 212 insertions(+), 49 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineLayoutSettings.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineLayoutSettingsProvider.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index b8d7d96ecf..5112604dd3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -108,6 +108,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat val informationData = messageInformationDataFactory.create(params) val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, params.callback) return MessageTextItem_() + .layout(informationData.messageLayout.layoutRes) .leftGuideline(avatarSizeProvider.leftGuideline) .highlighted(params.isHighlighted) .attributes(attributes) 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 c42d50e924..33d77fe16c 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 @@ -154,7 +154,7 @@ class MessageItemFactory @Inject constructor( // val all = event.root.toContent() // val ev = all.toModel<Event>() - return when (messageContent) { + val messageItem = when (messageContent) { is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) @@ -172,6 +172,9 @@ class MessageItemFactory @Inject constructor( is MessagePollContent -> buildPollContent(messageContent, informationData, highlight, callback, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } + return messageItem?.apply { + layout(informationData.messageLayout.layoutRes) + } } private fun buildPollContent(pollContent: MessagePollContent, @@ -389,13 +392,6 @@ 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) @@ -517,13 +513,6 @@ class MessageItemFactory @Inject constructor( linkifiedBody }.toEpoxyCharSequence() ) - .layout( - if (informationData.sentByMe) { - R.layout.item_timeline_event_bubble_outgoing_base - } else { - R.layout.item_timeline_event_bubble_incoming_base - } - ) .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString())) .bindingOptions(bindingOptions) .searchForPills(isFormatted) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt index 00b02c2cf0..a34c216fad 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt @@ -17,14 +17,22 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.timeline.style.TimelineLayoutSettings +import im.vector.app.features.home.room.detail.timeline.style.TimelineLayoutSettingsProvider import javax.inject.Inject -class AvatarSizeProvider @Inject constructor(private val dimensionConverter: DimensionConverter) { +class AvatarSizeProvider @Inject constructor(private val dimensionConverter: DimensionConverter, + private val layoutSettingsProvider: TimelineLayoutSettingsProvider) { - private val avatarStyle = AvatarStyle.X_SMALL + private val avatarStyle by lazy { + when (layoutSettingsProvider.getLayoutSettings()) { + TimelineLayoutSettings.MODERN -> AvatarStyle.SMALL + TimelineLayoutSettings.BUBBLE -> AvatarStyle.BUBBLE + } + } val leftGuideline: Int by lazy { - dimensionConverter.dpToPx(avatarStyle.avatarSizeDP + 4) + dimensionConverter.dpToPx(avatarStyle.avatarSizeDP + avatarStyle.marginDP) } val avatarSize: Int by lazy { @@ -33,12 +41,12 @@ class AvatarSizeProvider @Inject constructor(private val dimensionConverter: Dim companion object { - enum class AvatarStyle(val avatarSizeDP: Int) { - BIG(50), - MEDIUM(40), - SMALL(30), - X_SMALL(28), - NONE(0) + enum class AvatarStyle(val avatarSizeDP: Int, val marginDP: Int) { + BIG(50, 8), + MEDIUM(40, 8), + SMALL(30, 8), + BUBBLE(28, 4), + NONE(0, 8) } } } 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 b9ea78e0db..2edab8af74 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 @@ -27,7 +27,7 @@ import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration -import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory import org.matrix.android.sdk.api.crypto.VerificationState import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session @@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited -import org.matrix.android.sdk.api.session.room.timeline.isEdition import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import javax.inject.Inject @@ -51,8 +50,7 @@ import javax.inject.Inject */ class MessageInformationDataFactory @Inject constructor(private val session: Session, private val dateFormatter: VectorDateFormatter, - private val visibilityHelper: TimelineEventVisibilityHelper, - private val vectorPreferences: VectorPreferences) { + private val messageLayoutFactory: TimelineMessageLayoutFactory) { fun create(params: TimelineItemFactoryParams): MessageInformationData { val event = params.event @@ -66,21 +64,9 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses val nextDate = nextDisplayableEvent?.root?.localDateTime() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() - val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60)) - ?: false - val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId || prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() - val showInformation = - (addDaySeparator || - event.senderInfo.avatarUrl != nextDisplayableEvent?.senderInfo?.avatarUrl || - event.senderInfo.disambiguatedDisplayName != nextDisplayableEvent?.senderInfo?.disambiguatedDisplayName || - nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) || - isNextMessageReceivedMoreThanOneHourAgo || - isTileTypeMessage(nextDisplayableEvent) || - nextDisplayableEvent.isEdition()) && !isSentByMe - val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) val e2eDecoration = getE2EDecoration(roomSummary, event) @@ -95,6 +81,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses SendStateDecoration.NONE } + val messageLayout = messageLayoutFactory.create(params) + return MessageInformationData( eventId = eventId, senderId = event.root.senderId ?: "", @@ -103,9 +91,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ageLocalTS = event.root.ageLocalTs, avatarUrl = event.senderInfo.avatarUrl, memberName = event.senderInfo.disambiguatedDisplayName, - showAvatar = showInformation, - showDisplayName = showInformation, - showTimestamp = true, + messageLayout = messageLayout, orderedReactionList = event.annotations?.reactionsSummary // ?.filter { isSingleEmoji(it.key) } ?.map { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index a964af6f73..9f3b2bddf2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -62,7 +62,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H> override fun bind(holder: H) { super.bind(holder) - if (attributes.informationData.showAvatar) { + if (attributes.informationData.messageLayout.showAvatar) { holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply { height = attributes.avatarSize width = attributes.avatarSize @@ -76,7 +76,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H> holder.avatarImageView.setOnLongClickListener(null) holder.avatarImageView.isVisible = false } - if (attributes.informationData.showDisplayName) { + if (attributes.informationData.messageLayout.showDisplayName) { holder.memberNameView.isVisible = true holder.memberNameView.text = attributes.informationData.memberName holder.memberNameView.setTextColor(attributes.getMemberNameColor()) @@ -87,7 +87,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H> holder.memberNameView.setOnLongClickListener(null) holder.memberNameView.isVisible = false } - if (attributes.informationData.showTimestamp) { + if (attributes.informationData.messageLayout.showTimestamp) { holder.timeView.isVisible = true holder.timeView.text = attributes.informationData.time } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index 76fc9a5eff..629d20e898 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -17,6 +17,7 @@ package im.vector.app.features.home.room.detail.timeline.item import android.os.Parcelable +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.crypto.VerificationState import org.matrix.android.sdk.api.session.room.send.SendState @@ -31,9 +32,7 @@ data class MessageInformationData( val ageLocalTS: Long?, val avatarUrl: String?, val memberName: CharSequence? = null, - val showAvatar: Boolean, - val showDisplayName: Boolean, - val showTimestamp: Boolean, + val messageLayout: TimelineMessageLayout, /*List of reactions (emoji,count,isSelected)*/ val orderedReactionList: List<ReactionInfoData>? = null, val pollResponseAggregatedSummary: PollResponseData? = null, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineLayoutSettings.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineLayoutSettings.kt new file mode 100644 index 0000000000..873ef8c907 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineLayoutSettings.kt @@ -0,0 +1,22 @@ +/* + * 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.style + +enum class TimelineLayoutSettings { + MODERN, + BUBBLE +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineLayoutSettingsProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineLayoutSettingsProvider.kt new file mode 100644 index 0000000000..a10c95befe --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineLayoutSettingsProvider.kt @@ -0,0 +1,26 @@ +/* + * 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.style + +import javax.inject.Inject + +class TimelineLayoutSettingsProvider @Inject constructor() { + + fun getLayoutSettings(): TimelineLayoutSettings { + return TimelineLayoutSettings.BUBBLE + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt new file mode 100644 index 0000000000..48dba0a58a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt @@ -0,0 +1,44 @@ +/* + * 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.style + +import android.os.Parcelable +import im.vector.app.R +import kotlinx.parcelize.Parcelize + +sealed interface TimelineMessageLayout : Parcelable { + val layoutRes: Int + val showAvatar: Boolean + val showDisplayName: Boolean + val showTimestamp: Boolean + + @Parcelize + data class Modern(override val showAvatar: Boolean, + override val showDisplayName: Boolean, + override val showTimestamp: Boolean, + override val layoutRes: Int = R.layout.item_timeline_event_base) : TimelineMessageLayout + + @Parcelize + data class Bubble(override val showAvatar: Boolean, + override val showDisplayName: Boolean, + override val showTimestamp: Boolean = true, + val isIncoming: Boolean, + val isFirstFromThisSender: Boolean, + val isLastFromThisSender: Boolean, + override val layoutRes: Int = if (isIncoming) R.layout.item_timeline_event_bubble_incoming_base else R.layout.item_timeline_event_bubble_outgoing_base, + ) : TimelineMessageLayout +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt new file mode 100644 index 0000000000..18df8c133b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -0,0 +1,88 @@ +/* + * 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.style + +import im.vector.app.core.extensions.localDateTime +import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams +import im.vector.app.features.settings.VectorPreferences +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.MessageVerificationRequestContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent +import org.matrix.android.sdk.api.session.room.timeline.isEdition +import javax.inject.Inject + +class TimelineMessageLayoutFactory @Inject constructor(private val session: Session, + private val layoutSettingsProvider: TimelineLayoutSettingsProvider, + private val vectorPreferences: VectorPreferences) { + + fun create(params: TimelineItemFactoryParams): TimelineMessageLayout { + + val event = params.event + val nextDisplayableEvent = params.nextDisplayableEvent + val prevDisplayableEvent = params.prevDisplayableEvent + val isSentByMe = event.root.senderId == session.myUserId + + val date = event.root.localDateTime() + val nextDate = nextDisplayableEvent?.root?.localDateTime() + val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() + + val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60)) + ?: false + + val showInformation = + (addDaySeparator || + event.senderInfo.avatarUrl != nextDisplayableEvent?.senderInfo?.avatarUrl || + event.senderInfo.disambiguatedDisplayName != nextDisplayableEvent?.senderInfo?.disambiguatedDisplayName || + nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) || + isNextMessageReceivedMoreThanOneHourAgo || + isTileTypeMessage(nextDisplayableEvent) || + nextDisplayableEvent.isEdition()) && !isSentByMe + + val messageLayout = when (layoutSettingsProvider.getLayoutSettings()) { + TimelineLayoutSettings.MODERN -> TimelineMessageLayout.Modern(showInformation, showInformation, showInformation || vectorPreferences.alwaysShowTimeStamps()) + TimelineLayoutSettings.BUBBLE -> { + val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator + val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId || prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() + TimelineMessageLayout.Bubble( + showAvatar = showInformation, + showDisplayName = showInformation, + isIncoming = !isSentByMe, + isFirstFromThisSender = isFirstFromThisSender, + isLastFromThisSender = isLastFromThisSender + ) + } + } + return messageLayout + } + + /** + * Tiles type message never show the sender information (like verification request), so we should repeat it for next message + * even if same sender + */ + private fun isTileTypeMessage(event: TimelineEvent?): Boolean { + return when (event?.root?.getClearType()) { + EventType.KEY_VERIFICATION_DONE, + EventType.KEY_VERIFICATION_CANCEL -> true + EventType.MESSAGE -> { + event.getLastMessageContent() is MessageVerificationRequestContent + } + else -> false + } + } +} From f7df0b891efbf53d55f46f574d0da9a4de774a08 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Wed, 12 Jan 2022 18:45:40 +0100 Subject: [PATCH 06/36] Bubbles: fix recycling issue --- .../detail/timeline/factory/MessageItemFactory.kt | 4 +++- .../room/detail/timeline/item/BaseEventItem.kt | 15 +++++++++++++++ .../detail/timeline/item/CallTileTimelineItem.kt | 2 +- .../home/room/detail/timeline/item/DefaultItem.kt | 2 +- .../timeline/item/MergedMembershipEventsItem.kt | 2 +- .../timeline/item/MergedRoomCreationItem.kt | 2 +- .../detail/timeline/item/MessageBlockCodeItem.kt | 2 +- .../room/detail/timeline/item/MessageFileItem.kt | 2 +- .../detail/timeline/item/MessageImageVideoItem.kt | 2 +- .../room/detail/timeline/item/MessageTextItem.kt | 2 +- .../room/detail/timeline/item/MessageVoiceItem.kt | 2 +- .../home/room/detail/timeline/item/NoticeItem.kt | 2 +- .../home/room/detail/timeline/item/PollItem.kt | 2 ++ .../detail/timeline/item/RedactedMessageItem.kt | 2 +- .../timeline/item/StatusTileTimelineItem.kt | 2 +- .../timeline/item/VerificationRequestItem.kt | 2 +- .../timeline/item/WidgetTileTimelineItem.kt | 2 +- 17 files changed, 34 insertions(+), 15 deletions(-) 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 33d77fe16c..80343fd909 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 @@ -172,7 +172,9 @@ class MessageItemFactory @Inject constructor( is MessagePollContent -> buildPollContent(messageContent, informationData, highlight, callback, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } - return messageItem?.apply { + return messageItem?.takeIf { + it.layout == R.layout.item_timeline_event_base + }?.apply { layout(informationData.messageLayout.layoutRes) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt index 5dfbf5d8f6..3df6393c05 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -27,6 +27,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.utils.DimensionConverter +import timber.log.Timber /** * Children must override getViewType() @@ -43,6 +44,20 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H> @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var dimensionConverter: DimensionConverter + final override fun getViewType(): Int { + // This makes sure we have a unique integer for the combination of layout and ViewStubId. + return pairingFunction(layout, getViewStubId()).also { + Timber.v("GetViewType: for ${javaClass.canonicalName} $it with layout:$layout and stubId:${getViewStubId()}") + } + } + + abstract fun getViewStubId(): Int + + // Szudzik function + private fun pairingFunction(a: Int, b: Int): Int { + return if (a >= b) a * a + a + b else a + b * b + } + @CallSuper override fun bind(holder: H) { super.bind(holder) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt index 5f8ac822da..218d648318 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt @@ -50,7 +50,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho @EpoxyAttribute lateinit var attributes: Attributes - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID override fun bind(holder: Holder) { super.bind(holder) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt index 9d0a3b9a9c..9d437754d0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt @@ -46,7 +46,7 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() { return listOf(attributes.informationData.eventId) } - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID class Holder : BaseHolder(STUB_ID) { val avatarImageView by bind<ImageView>(R.id.itemDefaultAvatarView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt index a52ddf8336..e19dc33fff 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt @@ -29,7 +29,7 @@ import im.vector.app.features.home.AvatarRenderer @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() { - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID @EpoxyAttribute override lateinit var attributes: Attributes diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt index 1e8e96426f..9f631f7a0e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt @@ -51,7 +51,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var movementMethod: MovementMethod? = null - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID override fun bind(holder: Holder) { super.bind(holder) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt index be9b727017..9e162a8f1e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt @@ -44,7 +44,7 @@ abstract class MessageBlockCodeItem : AbsMessageItem<MessageBlockCodeItem.Holder holder.editedView.setTextOrHide(editedSpan?.charSequence) } - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { val messageView by bind<TextView>(R.id.codeBlockTextView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt index b15f909b79..bd35532eae 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -95,7 +95,7 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() { contentDownloadStateTrackerBinder.unbind(mxcUrl) } - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout) 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 e865354747..e20ad48b17 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 @@ -83,7 +83,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold super.unbind(holder) } - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index 6a438d3a06..e83611d6d8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -115,7 +115,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() { previewUrlRetriever?.removeListener(attributes.informationData.eventId, previewUrlViewUpdater) } - override fun getViewType() = STUB_ID + layout + override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { val messageView by bind<AppCompatTextView>(R.id.messageTextView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt index f006c2aa35..b6fa684eca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -120,7 +120,7 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() { voiceMessagePlaybackTracker.unTrack(attributes.informationData.eventId) } - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { val voiceLayout by bind<ViewGroup>(R.id.voiceLayout) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt index 2851668df5..e998b7e58f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt @@ -64,7 +64,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() { return listOf(attributes.informationData.eventId) } - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID class Holder : BaseHolder(STUB_ID) { val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt index 1308fa49c8..784eec87fe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollItem.kt @@ -46,6 +46,8 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() { @EpoxyAttribute lateinit var optionViewStates: List<PollOptionViewState> + override fun getViewStubId() = STUB_ID + override fun bind(holder: Holder) { super.bind(holder) val relatedEventId = eventId ?: return diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RedactedMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RedactedMessageItem.kt index 282550daec..204bab2254 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RedactedMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/RedactedMessageItem.kt @@ -22,7 +22,7 @@ import im.vector.app.R @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class RedactedMessageItem : AbsMessageItem<RedactedMessageItem.Holder>() { - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID override fun shouldShowReactionAtBottom() = false diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt index 6531efb82d..66bf41ddac 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt @@ -40,7 +40,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem<StatusTileTimelineIte @EpoxyAttribute lateinit var attributes: Attributes - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID @SuppressLint("SetTextI18n") override fun bind(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt index f4e093a428..1c1b064bf5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/VerificationRequestItem.kt @@ -51,7 +51,7 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI @EpoxyAttribute var callback: TimelineEventController.Callback? = null - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID @SuppressLint("SetTextI18n") override fun bind(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/WidgetTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/WidgetTileTimelineItem.kt index 07b55c8ec2..151a044b2e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/WidgetTileTimelineItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/WidgetTileTimelineItem.kt @@ -41,7 +41,7 @@ abstract class WidgetTileTimelineItem : AbsBaseMessageItem<WidgetTileTimelineIte @EpoxyAttribute lateinit var attributes: Attributes - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID @SuppressLint("SetTextI18n") override fun bind(holder: Holder) { From 32e72f54b3906a7d95361f39452e0af5452916c9 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Wed, 12 Jan 2022 19:01:13 +0100 Subject: [PATCH 07/36] Bubbles: add quick settings (temporary) --- .../timeline/style/TimelineLayoutSettingsProvider.kt | 9 +++++++-- .../vector/app/features/settings/VectorPreferences.kt | 10 ++++++++++ .../src/main/res/xml/vector_settings_preferences.xml | 5 +++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineLayoutSettingsProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineLayoutSettingsProvider.kt index a10c95befe..9e351a706d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineLayoutSettingsProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineLayoutSettingsProvider.kt @@ -16,11 +16,16 @@ package im.vector.app.features.home.room.detail.timeline.style +import im.vector.app.features.settings.VectorPreferences import javax.inject.Inject -class TimelineLayoutSettingsProvider @Inject constructor() { +class TimelineLayoutSettingsProvider @Inject constructor(private val vectorPreferences: VectorPreferences) { fun getLayoutSettings(): TimelineLayoutSettings { - return TimelineLayoutSettings.BUBBLE + return if (vectorPreferences.useMessageBubblesLayout()) { + TimelineLayoutSettings.BUBBLE + } else { + TimelineLayoutSettings.MODERN + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 3436c20ce3..9c472a387c 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -83,6 +83,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { // interface const val SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY = "SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY" const val SETTINGS_INTERFACE_TEXT_SIZE_KEY = "SETTINGS_INTERFACE_TEXT_SIZE_KEY" + const val SETTINGS_INTERFACE_BUBBLE_KEY = "SETTINGS_INTERFACE_BUBBLE_KEY" const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY" private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY" private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY" @@ -852,6 +853,15 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_SHOW_EMOJI_KEYBOARD, true) } + /** + * Tells if the emoji keyboard button should be visible or not. + * + * @return true to show emoji keyboard button. + */ + fun useMessageBubblesLayout(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_INTERFACE_BUBBLE_KEY, false) + } + /** * Tells if the rage shake is used. * diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 14c7dc7b80..ac8a48fb2e 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -22,6 +22,11 @@ android:title="@string/settings_theme" app:iconSpaceReserved="false" /> + <im.vector.app.core.preference.VectorSwitchPreference + android:defaultValue="false" + android:key="SETTINGS_INTERFACE_BUBBLE_KEY" + android:title="Message bubbles" /> + <im.vector.app.core.preference.VectorPreference android:dialogTitle="@string/font_size" android:key="SETTINGS_INTERFACE_TEXT_SIZE_KEY" From 37af93fba426d83971dd8131cee2ecd39fc6058b Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Wed, 12 Jan 2022 19:22:14 +0100 Subject: [PATCH 08/36] Bubbles: fix avatar/name visibility in modern layout --- .../style/TimelineMessageLayoutFactory.kt | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index 18df8c133b..bd37c2d66a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -45,23 +45,28 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60)) ?: false - val showInformation = - (addDaySeparator || - event.senderInfo.avatarUrl != nextDisplayableEvent?.senderInfo?.avatarUrl || - event.senderInfo.disambiguatedDisplayName != nextDisplayableEvent?.senderInfo?.disambiguatedDisplayName || - nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) || - isNextMessageReceivedMoreThanOneHourAgo || - isTileTypeMessage(nextDisplayableEvent) || - nextDisplayableEvent.isEdition()) && !isSentByMe + val showInformation = addDaySeparator || + event.senderInfo.avatarUrl != nextDisplayableEvent?.senderInfo?.avatarUrl || + event.senderInfo.disambiguatedDisplayName != nextDisplayableEvent?.senderInfo?.disambiguatedDisplayName || + nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) || + isNextMessageReceivedMoreThanOneHourAgo || + isTileTypeMessage(nextDisplayableEvent) || + nextDisplayableEvent.isEdition() val messageLayout = when (layoutSettingsProvider.getLayoutSettings()) { - TimelineLayoutSettings.MODERN -> TimelineMessageLayout.Modern(showInformation, showInformation, showInformation || vectorPreferences.alwaysShowTimeStamps()) + TimelineLayoutSettings.MODERN -> { + TimelineMessageLayout.Modern( + showAvatar = showInformation, + showDisplayName = showInformation, + showTimestamp = showInformation || vectorPreferences.alwaysShowTimeStamps() + ) + } TimelineLayoutSettings.BUBBLE -> { val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId || prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() TimelineMessageLayout.Bubble( - showAvatar = showInformation, - showDisplayName = showInformation, + showAvatar = showInformation && !isSentByMe, + showDisplayName = showInformation && !isSentByMe, isIncoming = !isSentByMe, isFirstFromThisSender = isFirstFromThisSender, isLastFromThisSender = isLastFromThisSender From b9cc7959967aa2098b99eb9977b97b13aa308d1f Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Thu, 13 Jan 2022 12:33:36 +0100 Subject: [PATCH 09/36] Bubbles : fix background colors --- library/ui-styles/src/main/res/values/colors.xml | 3 --- .../room/detail/timeline/view/MessageBubbleView.kt | 12 ++++++++---- .../res/drawable/bg_timeline_incoming_message.xml | 4 ---- .../res/drawable/bg_timeline_outgoing_message.xml | 4 ---- vector/src/main/res/layout/view_message_bubble.xml | 1 - 5 files changed, 8 insertions(+), 16 deletions(-) delete mode 100644 vector/src/main/res/drawable/bg_timeline_incoming_message.xml delete mode 100644 vector/src/main/res/drawable/bg_timeline_outgoing_message.xml diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index e3ec542c89..ca6f6d3142 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -13,9 +13,6 @@ <color name="button_bot_background_color">#14368BD6</color> <color name="button_bot_enabled_text_color">@color/palette_azure</color> - <color name="bubble_background_outgoing">#0F0DBD8B</color> - <color name="bubble_background_incoming">@color/element_system_light</color> - <!-- Notification (do not depends on theme) --> <color name="notification_accent_color">@color/palette_azure</color> <color name="key_share_req_accent_color">@color/palette_melon</color> diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 5bb732bdde..46e3edca44 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -17,6 +17,7 @@ package im.vector.app.features.home.room.detail.timeline.view import android.content.Context +import android.content.res.ColorStateList import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.View @@ -26,12 +27,14 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes +import androidx.core.graphics.ColorUtils import androidx.core.view.updateLayoutParams import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import im.vector.app.R import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.themes.ThemeUtils class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) @@ -116,7 +119,6 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } applyTo(bubbleView) } - } private fun createBackgroundDrawable(): Drawable { @@ -133,14 +135,16 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri val shapeAppearanceModelBuilder = ShapeAppearanceModel().toBuilder() val backgroundColor: Int if (isIncoming) { - backgroundColor = R.color.bubble_background_incoming + backgroundColor = ThemeUtils.getColor(context, R.attr.vctr_system) shapeAppearanceModelBuilder .setTopRightCorner(CornerFamily.ROUNDED, cornerRadius) .setBottomRightCorner(CornerFamily.ROUNDED, cornerRadius) .setTopLeftCorner(topCornerFamily, topRadius) .setBottomLeftCorner(bottomCornerFamily, bottomRadius) } else { - backgroundColor = R.color.bubble_background_outgoing + val resolvedColor = ContextCompat.getColor(context, R.color.palette_element_green) + val alpha = if (ThemeUtils.isLightTheme(context)) 0x0E else 0x26 + backgroundColor = ColorUtils.setAlphaComponent(resolvedColor, alpha) shapeAppearanceModelBuilder .setTopLeftCorner(CornerFamily.ROUNDED, cornerRadius) .setBottomLeftCorner(CornerFamily.ROUNDED, cornerRadius) @@ -149,7 +153,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } val shapeAppearanceModel = shapeAppearanceModelBuilder.build() return MaterialShapeDrawable(shapeAppearanceModel).apply { - fillColor = ContextCompat.getColorStateList(context, backgroundColor) + fillColor = ColorStateList.valueOf(backgroundColor) } } } diff --git a/vector/src/main/res/drawable/bg_timeline_incoming_message.xml b/vector/src/main/res/drawable/bg_timeline_incoming_message.xml deleted file mode 100644 index 2cbca33702..0000000000 --- a/vector/src/main/res/drawable/bg_timeline_incoming_message.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android"> - <solid android:color="?attr/vctr_system" /> -</shape> \ No newline at end of file diff --git a/vector/src/main/res/drawable/bg_timeline_outgoing_message.xml b/vector/src/main/res/drawable/bg_timeline_outgoing_message.xml deleted file mode 100644 index 0f75705a77..0000000000 --- a/vector/src/main/res/drawable/bg_timeline_outgoing_message.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<shape xmlns:android="http://schemas.android.com/apk/res/android"> - <solid android:color="#0F0DBD8B" /> -</shape> \ No newline at end of file diff --git a/vector/src/main/res/layout/view_message_bubble.xml b/vector/src/main/res/layout/view_message_bubble.xml index 8e9a95222a..8570a3dee2 100644 --- a/vector/src/main/res/layout/view_message_bubble.xml +++ b/vector/src/main/res/layout/view_message_bubble.xml @@ -91,7 +91,6 @@ android:layout_marginStart="0dp" android:layout_marginEnd="0dp" android:addStatesFromChildren="true" - android:background="@drawable/bg_timeline_incoming_message" android:paddingStart="4dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> From baee076e41c628ff3897b80097212f8b8fd40ae9 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Thu, 13 Jan 2022 12:33:58 +0100 Subject: [PATCH 10/36] Bubbles: fix types using wrong layout --- .../timeline/factory/MessageItemFactory.kt | 5 +- .../timeline/style/TimelineMessageLayout.kt | 9 +-- .../style/TimelineMessageLayoutFactory.kt | 56 ++++++++++++++----- 3 files changed, 49 insertions(+), 21 deletions(-) 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 80343fd909..a80b948428 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 @@ -172,9 +172,7 @@ class MessageItemFactory @Inject constructor( is MessagePollContent -> buildPollContent(messageContent, informationData, highlight, callback, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } - return messageItem?.takeIf { - it.layout == R.layout.item_timeline_event_base - }?.apply { + return messageItem?.apply { layout(informationData.messageLayout.layoutRes) } } @@ -650,6 +648,7 @@ class MessageItemFactory @Inject constructor( private fun buildRedactedItem(attributes: AbsMessageItem.Attributes, highlight: Boolean): RedactedMessageItem? { return RedactedMessageItem_() + .layout(attributes.informationData.messageLayout.layoutRes) .leftGuideline(avatarSizeProvider.leftGuideline) .attributes(attributes) .highlighted(highlight) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt index 48dba0a58a..50f4e95cee 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt @@ -27,10 +27,11 @@ sealed interface TimelineMessageLayout : Parcelable { val showTimestamp: Boolean @Parcelize - data class Modern(override val showAvatar: Boolean, - override val showDisplayName: Boolean, - override val showTimestamp: Boolean, - override val layoutRes: Int = R.layout.item_timeline_event_base) : TimelineMessageLayout + data class Default(override val showAvatar: Boolean, + override val showDisplayName: Boolean, + override val showTimestamp: Boolean, + // Keep defaultLayout generated on epoxy items + override val layoutRes: Int = 0) : TimelineMessageLayout @Parcelize data class Bubble(override val showAvatar: Boolean, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index bd37c2d66a..9b2877f0f8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -21,6 +21,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFact import im.vector.app.features.settings.VectorPreferences 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.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent @@ -31,6 +32,18 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess private val layoutSettingsProvider: TimelineLayoutSettingsProvider, private val vectorPreferences: VectorPreferences) { + companion object { + private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf( + EventType.MESSAGE, + EventType.ENCRYPTED, + EventType.STICKER + ) + private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf( + MessageType.MSGTYPE_POLL_START, + MessageType.MSGTYPE_VERIFICATION_REQUEST + ) + } + fun create(params: TimelineItemFactoryParams): TimelineMessageLayout { val event = params.event @@ -55,27 +68,42 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess val messageLayout = when (layoutSettingsProvider.getLayoutSettings()) { TimelineLayoutSettings.MODERN -> { - TimelineMessageLayout.Modern( - showAvatar = showInformation, - showDisplayName = showInformation, - showTimestamp = showInformation || vectorPreferences.alwaysShowTimeStamps() - ) + buildModernLayout(showInformation) } TimelineLayoutSettings.BUBBLE -> { - val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator - val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId || prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() - TimelineMessageLayout.Bubble( - showAvatar = showInformation && !isSentByMe, - showDisplayName = showInformation && !isSentByMe, - isIncoming = !isSentByMe, - isFirstFromThisSender = isFirstFromThisSender, - isLastFromThisSender = isLastFromThisSender - ) + val type = event.root.getClearType() + if (type in EVENT_TYPES_WITH_BUBBLE_LAYOUT) { + val messageContent = if (type == EventType.MESSAGE) params.event.getLastMessageContent() else null + if (messageContent?.msgType in MSG_TYPES_WITHOUT_BUBBLE_LAYOUT) { + buildModernLayout(showInformation) + } + val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator + val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId + || prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() + + TimelineMessageLayout.Bubble( + showAvatar = showInformation && !isSentByMe, + showDisplayName = showInformation && !isSentByMe, + isIncoming = !isSentByMe, + isFirstFromThisSender = isFirstFromThisSender, + isLastFromThisSender = isLastFromThisSender + ) + } else { + buildModernLayout(showInformation) + } } } return messageLayout } + private fun buildModernLayout(showInformation: Boolean): TimelineMessageLayout.Default { + return TimelineMessageLayout.Default( + showAvatar = showInformation, + showDisplayName = showInformation, + showTimestamp = showInformation || vectorPreferences.alwaysShowTimeStamps() + ) + } + /** * Tiles type message never show the sender information (like verification request), so we should repeat it for next message * even if same sender From 5ac155285bcbf13992e3e2ff2d9352f30bbe0cb3 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Thu, 13 Jan 2022 13:14:37 +0100 Subject: [PATCH 11/36] Bubbles: some clean up --- tools/check/forbidden_strings_in_code.txt | 2 +- .../app/features/home/AvatarRenderer.kt | 1 - .../helper/MessageInformationDataFactory.kt | 3 ++- .../timeline/style/TimelineMessageLayout.kt | 21 ++++++++++++------- .../style/TimelineMessageLayoutFactory.kt | 5 ++--- .../detail/timeline/view/MessageBubbleView.kt | 4 ++-- vector/src/main/res/values/strings.xml | 1 + .../res/xml/vector_settings_preferences.xml | 2 +- 8 files changed, 22 insertions(+), 17 deletions(-) diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index a0c7c2f4c6..cbfaf20247 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -160,7 +160,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===115 +enum class===116 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index be689eb27a..2ee3233637 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -145,7 +145,6 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active } else -> { it.apply(RequestOptions.circleCropTransform()) - } } } 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 2edab8af74..276802e574 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 @@ -65,7 +65,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator - val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId || prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() + val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId || + prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE) val e2eDecoration = getE2EDecoration(roomSummary, event) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt index 50f4e95cee..bd4be10783 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt @@ -30,16 +30,21 @@ sealed interface TimelineMessageLayout : Parcelable { data class Default(override val showAvatar: Boolean, override val showDisplayName: Boolean, override val showTimestamp: Boolean, - // Keep defaultLayout generated on epoxy items + // Keep defaultLayout generated on epoxy items override val layoutRes: Int = 0) : TimelineMessageLayout @Parcelize - data class Bubble(override val showAvatar: Boolean, - override val showDisplayName: Boolean, - override val showTimestamp: Boolean = true, - val isIncoming: Boolean, - val isFirstFromThisSender: Boolean, - val isLastFromThisSender: Boolean, - override val layoutRes: Int = if (isIncoming) R.layout.item_timeline_event_bubble_incoming_base else R.layout.item_timeline_event_bubble_outgoing_base, + data class Bubble( + override val showAvatar: Boolean, + override val showDisplayName: Boolean, + override val showTimestamp: Boolean = true, + val isIncoming: Boolean, + val isFirstFromThisSender: Boolean, + val isLastFromThisSender: Boolean, + override val layoutRes: Int = if (isIncoming) { + R.layout.item_timeline_event_bubble_incoming_base + } else { + R.layout.item_timeline_event_bubble_outgoing_base + }, ) : TimelineMessageLayout } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index 9b2877f0f8..6c26679105 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -45,7 +45,6 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess } fun create(params: TimelineItemFactoryParams): TimelineMessageLayout { - val event = params.event val nextDisplayableEvent = params.nextDisplayableEvent val prevDisplayableEvent = params.prevDisplayableEvent @@ -78,8 +77,8 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess buildModernLayout(showInformation) } val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator - val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId - || prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() + val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId || + prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() TimelineMessageLayout.Bubble( showAvatar = showInformation && !isSentByMe, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 46e3edca44..68e32861c3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -37,8 +37,8 @@ import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.themes.ThemeUtils class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0) - : RelativeLayout(context, attrs, defStyleAttr), MessageViewConfiguration { + defStyleAttr: Int = 0) : + RelativeLayout(context, attrs, defStyleAttr), MessageViewConfiguration { override var isIncoming: Boolean = false set(value) { diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 34ac5fcddc..4e927701d7 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3701,4 +3701,5 @@ <string name="poll_end_room_list_preview">Poll ended</string> <string name="delete_poll_dialog_title">Remove poll</string> <string name="delete_poll_dialog_content">Are you sure you want to remove this poll? You won\'t be able to recover it once removed.</string> + <string name="message_bubbles">Message bubbles</string> </resources> diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index ac8a48fb2e..a25562e3d2 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -25,7 +25,7 @@ <im.vector.app.core.preference.VectorSwitchPreference android:defaultValue="false" android:key="SETTINGS_INTERFACE_BUBBLE_KEY" - android:title="Message bubbles" /> + android:title="@string/message_bubbles" /> <im.vector.app.core.preference.VectorPreference android:dialogTitle="@string/font_size" From 1108ef9fbe16b3768a340e161de99f5105b805a3 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Fri, 14 Jan 2022 19:19:23 +0100 Subject: [PATCH 12/36] Bubbles: make it works for file, voice and polls. Also add parity for "modern" layout. --- .../src/main/res/drawable/bg_media_pill.xml | 3 - .../main/res/drawable/file_progress_bar.xml | 8 +- .../ui-styles/src/main/res/values/dimens.xml | 3 + .../src/main/res/values/styles_progress.xml | 1 + .../src/main/res/values/styles_timeline.xml | 10 ++ .../ContentDownloadStateTrackerBinder.kt | 12 +-- .../detail/timeline/item/MessageFileItem.kt | 15 ++- .../timeline/item/MessageImageVideoItem.kt | 5 +- .../detail/timeline/item/MessageVoiceItem.kt | 13 +++ .../style/TimelineMessageLayoutFactory.kt | 27 ++--- .../detail/timeline/view/MessageBubbleView.kt | 23 +++- .../res/drawable/overlay_bubble_media.xml | 8 ++ .../res/layout/item_timeline_event_base.xml | 4 +- .../layout/item_timeline_event_file_stub.xml | 102 ++++++++---------- ...item_timeline_event_media_message_stub.xml | 12 +++ .../res/layout/item_timeline_event_poll.xml | 4 +- ...em_timeline_event_view_stubs_container.xml | 5 +- .../layout/item_timeline_event_voice_stub.xml | 29 ++--- vector/src/main/res/layout/view_file_icon.xml | 14 +-- .../main/res/layout/view_message_bubble.xml | 2 +- .../layout/view_voice_message_recorder.xml | 2 +- 21 files changed, 171 insertions(+), 131 deletions(-) rename vector/src/main/res/drawable/bg_voice_playback.xml => library/ui-styles/src/main/res/drawable/bg_media_pill.xml (84%) create mode 100644 vector/src/main/res/drawable/overlay_bubble_media.xml diff --git a/vector/src/main/res/drawable/bg_voice_playback.xml b/library/ui-styles/src/main/res/drawable/bg_media_pill.xml similarity index 84% rename from vector/src/main/res/drawable/bg_voice_playback.xml rename to library/ui-styles/src/main/res/drawable/bg_media_pill.xml index 4474c00345..2ad9ca9918 100644 --- a/vector/src/main/res/drawable/bg_voice_playback.xml +++ b/library/ui-styles/src/main/res/drawable/bg_media_pill.xml @@ -2,9 +2,6 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Tint color is provided by the theme --> <solid android:color="@android:color/black" /> - <size - android:width="240dp" - android:height="44dp" /> <corners android:bottomLeftRadius="12dp" android:bottomRightRadius="12dp" diff --git a/library/ui-styles/src/main/res/drawable/file_progress_bar.xml b/library/ui-styles/src/main/res/drawable/file_progress_bar.xml index d175b871f7..bc20bb6340 100644 --- a/library/ui-styles/src/main/res/drawable/file_progress_bar.xml +++ b/library/ui-styles/src/main/res/drawable/file_progress_bar.xml @@ -2,16 +2,14 @@ <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@android:id/background"> - <shape> - <corners android:radius="8dp" /> - <solid android:color="?vctr_room_active_widgets_banner_bg" /> + <shape android:shape="oval"> + <solid android:color="?vctr_system" /> </shape> </item> <item android:id="@android:id/progress"> <clip> - <shape> - <corners android:radius="8dp" /> + <shape android:shape="oval"> <solid android:color="@color/vctr_notice_secondary_alpha12" /> </shape> </clip> diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index 6b6f1a3aef..3955bf44fd 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -15,6 +15,8 @@ <dimen name="item_decoration_left_margin">72dp</dimen> <dimen name="item_event_message_state_size">16dp</dimen> + <dimen name="item_event_message_media_button_size">32dp</dimen> + <dimen name="chat_avatar_size">40dp</dimen> <dimen name="member_list_avatar_size">60dp</dimen> @@ -51,6 +53,7 @@ <dimen name="chat_bubble_margin_start">28dp</dimen> <dimen name="chat_bubble_margin_end">62dp</dimen> + <dimen name="chat_bubble_fixed_size">300dp</dimen> <!-- Onboarding --> <item name="ftue_auth_gutter_start_percent" format="float" type="dimen">0.05</item> diff --git a/library/ui-styles/src/main/res/values/styles_progress.xml b/library/ui-styles/src/main/res/values/styles_progress.xml index 712e7e98b6..04a0e01b58 100644 --- a/library/ui-styles/src/main/res/values/styles_progress.xml +++ b/library/ui-styles/src/main/res/values/styles_progress.xml @@ -6,6 +6,7 @@ <style name="Widget.Vector.ProgressBar.Horizontal.File"> <item name="android:indeterminateOnly">false</item> <item name="android:progressDrawable">@drawable/file_progress_bar</item> + <item name="android:progressBackgroundTint">?android:colorBackground</item> <item name="android:minHeight">10dp</item> <item name="android:maxHeight">40dp</item> </style> diff --git a/library/ui-styles/src/main/res/values/styles_timeline.xml b/library/ui-styles/src/main/res/values/styles_timeline.xml index 7fd7eac0ec..ef2f694d3e 100644 --- a/library/ui-styles/src/main/res/values/styles_timeline.xml +++ b/library/ui-styles/src/main/res/values/styles_timeline.xml @@ -12,4 +12,14 @@ <item name="android:layout_marginTop">4dp</item> </style> + <style name="TimelineContentMediaPillStyle"> + <item name="android:paddingStart">8dp</item> + <item name="android:paddingEnd">8dp</item> + <item name="android:paddingTop">6dp</item> + <item name="android:paddingBottom">6dp</item> + <item name="minHeight">48dp</item> + <item name="android:background">@drawable/bg_media_pill</item> + <item name="android:backgroundTint">?vctr_content_quinary</item> + </style> + </resources> \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt index caf0131144..e4405570a6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt @@ -29,9 +29,7 @@ import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker import javax.inject.Inject @ActivityScoped -class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, - private val messageColorProvider: MessageColorProvider, - private val errorFormatter: ErrorFormatter) { +class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){ private val updateListeners = mutableMapOf<String, ContentDownloadUpdater>() @@ -39,7 +37,7 @@ class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSe holder: MessageFileItem.Holder) { activeSessionHolder.getSafeActiveSession()?.also { session -> val downloadStateTracker = session.contentDownloadProgressTracker() - val updateListener = ContentDownloadUpdater(holder, messageColorProvider, errorFormatter) + val updateListener = ContentDownloadUpdater(holder) updateListeners[mxcUrl] = updateListener downloadStateTracker.track(mxcUrl, updateListener) } @@ -62,9 +60,7 @@ class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSe } } -private class ContentDownloadUpdater(private val holder: MessageFileItem.Holder, - private val messageColorProvider: MessageColorProvider, - private val errorFormatter: ErrorFormatter) : ContentDownloadStateTracker.UpdateListener { +private class ContentDownloadUpdater(private val holder: MessageFileItem.Holder) : ContentDownloadStateTracker.UpdateListener { override fun onDownloadStateUpdate(state: ContentDownloadStateTracker.State) { when (state) { @@ -124,7 +120,7 @@ private class ContentDownloadUpdater(private val holder: MessageFileItem.Holder, private fun handleSuccess() { stop() holder.fileDownloadProgress.isIndeterminate = false - holder.fileDownloadProgress.progress = 100 + holder.fileDownloadProgress.progress = 0 holder.fileImageView.setImageResource(R.drawable.ic_paperclip) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt index bd35532eae..e736c0e2da 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -16,6 +16,8 @@ package im.vector.app.features.home.room.detail.timeline.item +import android.content.res.ColorStateList +import android.graphics.Color import android.graphics.Paint import android.view.ViewGroup import android.widget.ImageView @@ -29,6 +31,8 @@ import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder 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 @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() { @@ -73,15 +77,19 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() { } else { if (izDownloaded) { holder.fileImageView.setImageResource(iconRes) - holder.fileDownloadProgress.progress = 100 + holder.fileDownloadProgress.progress = 0 } else { contentDownloadStateTrackerBinder.bind(mxcUrl, holder) holder.fileImageView.setImageResource(R.drawable.ic_download) - holder.fileDownloadProgress.progress = 0 } } // holder.view.setOnClickListener(clickListener) - + val backgroundTint = if(attributes.informationData.messageLayout is TimelineMessageLayout.Bubble){ + Color.TRANSPARENT + }else { + ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary) + } + holder.mainLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint) holder.filenameView.onClick(attributes.itemClickListener) holder.filenameView.setOnLongClickListener(attributes.itemLongClickListener) holder.fileImageWrapper.onClick(attributes.itemClickListener) @@ -98,6 +106,7 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() { override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { + val mainLayout by bind<ViewGroup>(R.id.messageFileMainLayout) val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout) val fileLayout by bind<ViewGroup>(R.id.messageFileLayout) val fileImageView by bind<ImageView>(R.id.messageFileIconView) 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 e20ad48b17..799cb1a002 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,7 @@ 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.style.TimelineMessageLayout import im.vector.app.features.home.room.detail.timeline.view.MessageViewConfiguration import im.vector.app.features.media.ImageContentRenderer @@ -71,7 +72,8 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold holder.mediaContentView.onClick(attributes.itemClickListener) holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener) holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE - (holder.view as? MessageViewConfiguration)?.showTimeAsOverlay = false + (holder.view as? MessageViewConfiguration)?.showTimeAsOverlay = true + holder.overlayView.isVisible = baseAttributes.informationData.messageLayout is TimelineMessageLayout.Bubble } override fun unbind(holder: Holder) { @@ -90,6 +92,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold val imageView by bind<ImageView>(R.id.messageThumbnailView) val playContentView by bind<ImageView>(R.id.messageMediaPlayView) val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia) + val overlayView by bind<View>(R.id.messageMediaOverlayView) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt index b6fa684eca..1058e3c1f9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -16,10 +16,14 @@ package im.vector.app.features.home.room.detail.timeline.item +import android.content.res.ColorStateList +import android.graphics.Color import android.text.format.DateUtils +import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass @@ -29,6 +33,8 @@ import im.vector.app.core.epoxy.ClickListener import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +import im.vector.app.features.themes.ThemeUtils @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() { @@ -80,6 +86,12 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() { } } + val backgroundTint = if(attributes.informationData.messageLayout is TimelineMessageLayout.Bubble){ + Color.TRANSPARENT + }else { + ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary) + } + holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint) holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) } voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener { @@ -123,6 +135,7 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() { override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { + val voicePlaybackLayout by bind<View>(R.id.voicePlaybackLayout) val voiceLayout by bind<ViewGroup>(R.id.voiceLayout) val voicePlaybackControlButton by bind<ImageButton>(R.id.voicePlaybackControlButton) val voicePlaybackTime by bind<TextView>(R.id.voicePlaybackTime) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index 6c26679105..f5ca97dc1d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -35,11 +35,11 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess companion object { private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf( EventType.MESSAGE, + EventType.POLL_START, EventType.ENCRYPTED, EventType.STICKER ) private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf( - MessageType.MSGTYPE_POLL_START, MessageType.MSGTYPE_VERIFICATION_REQUEST ) } @@ -72,21 +72,22 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess TimelineLayoutSettings.BUBBLE -> { val type = event.root.getClearType() if (type in EVENT_TYPES_WITH_BUBBLE_LAYOUT) { - val messageContent = if (type == EventType.MESSAGE) params.event.getLastMessageContent() else null + val messageContent = event.getLastMessageContent() if (messageContent?.msgType in MSG_TYPES_WITHOUT_BUBBLE_LAYOUT) { buildModernLayout(showInformation) - } - val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator - val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId || - prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() + } else { + val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator + val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId || + prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() - TimelineMessageLayout.Bubble( - showAvatar = showInformation && !isSentByMe, - showDisplayName = showInformation && !isSentByMe, - isIncoming = !isSentByMe, - isFirstFromThisSender = isFirstFromThisSender, - isLastFromThisSender = isLastFromThisSender - ) + TimelineMessageLayout.Bubble( + showAvatar = showInformation && !isSentByMe, + showDisplayName = showInformation && !isSentByMe, + isIncoming = !isSentByMe, + isFirstFromThisSender = isFirstFromThisSender, + isLastFromThisSender = isLastFromThisSender, + ) + } } else { buildModernLayout(showInformation) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 68e32861c3..0a3746f8a4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -23,6 +23,7 @@ import android.util.AttributeSet import android.view.View import android.view.ViewOutlineProvider import android.widget.RelativeLayout +import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat @@ -38,7 +39,7 @@ import im.vector.app.features.themes.ThemeUtils class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : - RelativeLayout(context, attrs, defStyleAttr), MessageViewConfiguration { + RelativeLayout(context, attrs, defStyleAttr), MessageViewConfiguration { override var isIncoming: Boolean = false set(value) { @@ -57,7 +58,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri render() } - override var showTimeAsOverlay: Boolean = true + override var showTimeAsOverlay: Boolean = false set(value) { field = value render() @@ -69,7 +70,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri inflate(context, R.layout.view_message_bubble, this) context.withStyledAttributes(attrs, R.styleable.MessageBubble) { isIncoming = getBoolean(R.styleable.MessageBubble_incoming_style, false) - showTimeAsOverlay = getBoolean(R.styleable.MessageBubble_show_time_overlay, true) + showTimeAsOverlay = getBoolean(R.styleable.MessageBubble_show_time_overlay, false) isFirstFromSender = getBoolean(R.styleable.MessageBubble_is_first, false) isLastFromSender = getBoolean(R.styleable.MessageBubble_is_last, false) } @@ -95,6 +96,9 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri findViewById<View>(R.id.messageEndGuideline).updateLayoutParams<LayoutParams> { marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end) } + findViewById<View>(R.id.messageStartGuideline).updateLayoutParams<LayoutParams> { + marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start) + } } else { val oppositeLayoutDirection = if (currentLayoutDirection == View.LAYOUT_DIRECTION_LTR) { View.LAYOUT_DIRECTION_RTL @@ -108,14 +112,23 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri findViewById<View>(R.id.messageEndGuideline).updateLayoutParams<LayoutParams> { marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start) } + findViewById<View>(R.id.messageStartGuideline).updateLayoutParams<LayoutParams> { + marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end) + } } ConstraintSet().apply { clone(bubbleView) clear(R.id.viewStubContainer, ConstraintSet.END) if (showTimeAsOverlay) { - connect(R.id.viewStubContainer, ConstraintSet.END, R.id.messageTimeView, ConstraintSet.START, 0) - } else { + val timeColor = ContextCompat.getColor(context, R.color.palette_white) + findViewById<TextView>(R.id.messageTimeView).setTextColor(timeColor) connect(R.id.viewStubContainer, ConstraintSet.END, R.id.parent, ConstraintSet.END, 0) + val margin = resources.getDimensionPixelSize(R.dimen.layout_horizontal_margin) + setMargin(R.id.messageTimeView, ConstraintSet.END, margin) + } else { + val timeColor = ThemeUtils.getColor(context, R.attr.vctr_content_tertiary) + findViewById<TextView>(R.id.messageTimeView).setTextColor(timeColor) + connect(R.id.viewStubContainer, ConstraintSet.END, R.id.messageTimeView, ConstraintSet.START, 0) } applyTo(bubbleView) } diff --git a/vector/src/main/res/drawable/overlay_bubble_media.xml b/vector/src/main/res/drawable/overlay_bubble_media.xml new file mode 100644 index 0000000000..ce34a39037 --- /dev/null +++ b/vector/src/main/res/drawable/overlay_bubble_media.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <gradient + android:type="linear" + android:angle="270" + android:startColor="#00000000" + android:endColor="#33000000"/> +</shape> \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index debf578519..bc9ed68232 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -78,14 +78,14 @@ <include android:id="@+id/viewStubContainer" + layout="@layout/item_timeline_event_view_stubs_container" android:layout_width="0dp" android:layout_height="wrap_content" - layout="@layout/item_timeline_event_view_stubs_container" android:layout_below="@id/messageMemberNameView" android:layout_marginEnd="8dp" android:layout_toStartOf="@id/messageSendStateImageView" android:layout_toEndOf="@id/messageStartGuideline" - android:addStatesFromChildren="true"/> + android:addStatesFromChildren="true" /> <im.vector.app.core.ui.views.SendStateImageView android:id="@+id/messageSendStateImageView" diff --git a/vector/src/main/res/layout/item_timeline_event_file_stub.xml b/vector/src/main/res/layout/item_timeline_event_file_stub.xml index 8a422ed177..41e4a118a3 100644 --- a/vector/src/main/res/layout/item_timeline_event_file_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_file_stub.xml @@ -1,80 +1,62 @@ <?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + +<LinearLayout 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/messageFileLayout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingTop="8dp" - android:paddingBottom="8dp" + android:orientation="vertical" tools:viewBindingIgnore="true"> - <im.vector.app.core.ui.views.ShieldImageView - android:id="@+id/messageFilee2eIcon" - android:layout_width="14dp" - android:layout_height="14dp" - android:visibility="gone" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:visibility="visible" /> - - <!-- the media type --> - <RelativeLayout - android:id="@+id/messageFileImageView" - android:layout_width="40dp" - android:layout_height="40dp" - android:layout_marginStart="4dp" - app:layout_constraintStart_toEndOf="@id/messageFilee2eIcon" - app:layout_constraintTop_toTopOf="parent"> - - <include layout="@layout/view_file_icon" /> - </RelativeLayout> - - <!-- <ImageView--> - <!-- android:id="@+id/messageFileImageView"--> - <!-- android:layout_width="@dimen/chat_avatar_size"--> - <!-- android:layout_height="@dimen/chat_avatar_size"--> - <!-- android:layout_marginStart="4dp"--> - <!-- app:layout_constraintStart_toEndOf="@id/messageFilee2eIcon"--> - <!-- app:layout_constraintTop_toTopOf="parent"--> - <!-- tools:src="@drawable/filetype_attachment" />--> - - <!-- the media --> - <TextView - android:id="@+id/messageFilenameView" - style="@style/Widget.Vector.TextView.Body" + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/messageFileMainLayout" + style="@style/TimelineContentMediaPillStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="4dp" - android:layout_marginEnd="32dp" - android:autoLink="none" - android:gravity="center_vertical" - android:minHeight="@dimen/chat_avatar_size" - app:layout_constrainedWidth="true" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHorizontal_bias="0" - app:layout_constraintStart_toEndOf="@id/messageFileImageView" - app:layout_constraintTop_toTopOf="parent" - tools:text="A filename here" /> + tools:viewBindingIgnore="true"> - <androidx.constraintlayout.widget.Barrier - android:id="@+id/horizontalBarrier" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierDirection="bottom" - app:constraint_referenced_ids="messageFileImageView,messageFilenameView" /> + <FrameLayout + android:id="@+id/messageFileImageView" + android:layout_width="32dp" + android:layout_height="32dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent"> + <include layout="@layout/view_file_icon" /> + + </FrameLayout> + + <!-- the file name--> + <TextView + android:id="@+id/messageFilenameView" + style="@style/Widget.Vector.TextView.Body" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:autoLink="none" + android:maxLines="1" + android:ellipsize="end" + android:gravity="center_vertical" + app:layout_constrainedWidth="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/messageFileImageView" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + tools:text="A filename here" /> + + </androidx.constraintlayout.widget.ConstraintLayout> <include android:id="@+id/messageFileUploadProgressLayout" layout="@layout/media_upload_download_progress_layout" - android:layout_width="0dp" - android:layout_height="46dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" android:layout_marginTop="8dp" android:layout_marginEnd="32dp" android:visibility="gone" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/horizontalBarrier" tools:visibility="visible" /> -</androidx.constraintlayout.widget.ConstraintLayout> + +</LinearLayout> \ No newline at end of file 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 68abfbfda8..bdfa0c5164 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 @@ -18,12 +18,24 @@ tools:layout_height="300dp" tools:src="@tools:sample/backgrounds/scenic" /> + <View + android:id="@+id/messageMediaOverlayView" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@drawable/overlay_bubble_media" + android:elevation="2dp" + app:layout_constraintStart_toStartOf="@id/messageThumbnailView" + app:layout_constraintEnd_toEndOf="@id/messageThumbnailView" + app:layout_constraintTop_toTopOf="@id/messageThumbnailView" + app:layout_constraintBottom_toBottomOf="@id/messageThumbnailView"/> + <ImageView android:id="@+id/messageMediaPlayView" android:layout_width="40dp" android:layout_height="40dp" android:contentDescription="@string/action_play" android:src="@drawable/ic_material_play_circle" + app:tint="?vctr_system" android:visibility="gone" app:layout_constraintBottom_toBottomOf="@id/messageThumbnailView" app:layout_constraintEnd_toEndOf="@id/messageThumbnailView" diff --git a/vector/src/main/res/layout/item_timeline_event_poll.xml b/vector/src/main/res/layout/item_timeline_event_poll.xml index 575b2b4d4e..64444e2c69 100644 --- a/vector/src/main/res/layout/item_timeline_event_poll.xml +++ b/vector/src/main/res/layout/item_timeline_event_poll.xml @@ -2,17 +2,19 @@ <androidx.constraintlayout.widget.ConstraintLayout 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:minWidth="@dimen/chat_bubble_fixed_size" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/questionTextView" style="@style/Widget.Vector.TextView.Subtitle" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:textColor="?vctr_content_primary" android:textStyle="bold" + app:layout_constraintHorizontal_bias="0" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml index 680db8ca51..576b2d4f01 100644 --- a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml +++ b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml @@ -30,6 +30,7 @@ android:id="@+id/messageContentFileStub" style="@style/TimelineContentStubBaseParams" android:layout_height="wrap_content" + tools:visibility="gone" android:layout="@layout/item_timeline_event_file_stub" /> <ViewStub @@ -41,12 +42,14 @@ <ViewStub android:id="@+id/messageContentVoiceStub" style="@style/TimelineContentStubBaseParams" + android:layout_height="wrap_content" android:layout="@layout/item_timeline_event_voice_stub" - tools:visibility="visible" /> + tools:visibility="gone" /> <ViewStub android:id="@+id/messageContentPollStub" style="@style/TimelineContentStubBaseParams" + android:layout_height="wrap_content" android:layout="@layout/item_timeline_event_poll" /> diff --git a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml index 5cdd5a815a..a180afbf8e 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_stub.xml @@ -1,31 +1,24 @@ <?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" +<LinearLayout 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/voiceLayout" + android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" + android:minWidth="@dimen/chat_bubble_fixed_size" tools:viewBindingIgnore="true"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/voicePlaybackLayout" - android:layout_width="0dp" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@drawable/bg_voice_playback" - android:backgroundTint="?vctr_content_quinary" - android:minHeight="48dp" - android:paddingStart="8dp" - android:paddingTop="6dp" - android:paddingEnd="8dp" - android:paddingBottom="6dp" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> + style="@style/TimelineContentMediaPillStyle"> <ImageButton android:id="@+id/voicePlaybackControlButton" - android:layout_width="32dp" - android:layout_height="32dp" + android:layout_width="@dimen/item_event_message_media_button_size" + android:layout_height="@dimen/item_event_message_media_button_size" android:background="@drawable/bg_voice_play_pause_button" android:backgroundTint="?android:colorBackground" android:contentDescription="@string/a11y_play_voice_message" @@ -65,16 +58,12 @@ <include android:id="@+id/messageFileUploadProgressLayout" layout="@layout/media_upload_download_progress_layout" - android:layout_width="0dp" + android:layout_width="match_parent" android:layout_height="46dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="32dp" android:visibility="gone" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/voicePlaybackLayout" tools:visibility="visible" /> -</androidx.constraintlayout.widget.ConstraintLayout> +</LinearLayout> diff --git a/vector/src/main/res/layout/view_file_icon.xml b/vector/src/main/res/layout/view_file_icon.xml index 1c5268a50b..db88802ba1 100644 --- a/vector/src/main/res/layout/view_file_icon.xml +++ b/vector/src/main/res/layout/view_file_icon.xml @@ -2,22 +2,22 @@ <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:layout_width="50dp" - android:layout_height="50dp" - tools:parentTag="android.widget.RelativeLayout"> + android:layout_width="32dp" + android:layout_height="32dp" + tools:parentTag="android.widget.FrameLayout"> <ProgressBar android:id="@+id/messageFileProgressbar" style="@style/Widget.Vector.ProgressBar.Horizontal.File" android:layout_width="match_parent" android:layout_height="match_parent" - android:progress="40" /> + tools:progress="40" /> <ImageView android:id="@+id/messageFileIconView" - android:layout_width="20dp" - android:layout_height="20dp" - android:layout_centerInParent="true" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_gravity="center" android:contentDescription="@string/attachment_type_file" android:src="@drawable/ic_download" app:tint="?vctr_notice_secondary" diff --git a/vector/src/main/res/layout/view_message_bubble.xml b/vector/src/main/res/layout/view_message_bubble.xml index 8570a3dee2..72e8f7dc29 100644 --- a/vector/src/main/res/layout/view_message_bubble.xml +++ b/vector/src/main/res/layout/view_message_bubble.xml @@ -105,7 +105,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintWidth_max="300dp" /> + app:layout_constraintWidth_max="@dimen/chat_bubble_fixed_size" /> <TextView android:id="@+id/messageTimeView" diff --git a/vector/src/main/res/layout/view_voice_message_recorder.xml b/vector/src/main/res/layout/view_voice_message_recorder.xml index 31a513d00b..70711e71a6 100644 --- a/vector/src/main/res/layout/view_voice_message_recorder.xml +++ b/vector/src/main/res/layout/view_voice_message_recorder.xml @@ -160,7 +160,7 @@ <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="0dp" android:layout_height="0dp" - android:background="@drawable/bg_voice_playback" + android:background="@drawable/bg_media_pill" android:backgroundTint="?vctr_content_quinary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" From a9e7c45074343210d73bee0f927521fe9de7aa4d Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Tue, 18 Jan 2022 19:26:23 +0100 Subject: [PATCH 13/36] Fix url preview sizing --- ...ageViewConfiguration.kt => TimelineMessageLayoutRenderer.kt} | 2 ++ vector/src/main/res/layout/view_url_preview.xml | 2 ++ 2 files changed, 4 insertions(+) rename vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/{MessageViewConfiguration.kt => TimelineMessageLayoutRenderer.kt} (94%) 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/TimelineMessageLayoutRenderer.kt similarity index 94% rename from vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageViewConfiguration.kt rename to vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/TimelineMessageLayoutRenderer.kt index bfb5884674..23dd9860f8 100644 --- 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/TimelineMessageLayoutRenderer.kt @@ -21,4 +21,6 @@ interface MessageViewConfiguration { var isFirstFromSender: Boolean var isLastFromSender: Boolean var showTimeAsOverlay: Boolean + var showNoBubble: Boolean + fun render() } diff --git a/vector/src/main/res/layout/view_url_preview.xml b/vector/src/main/res/layout/view_url_preview.xml index 93ea4ea2bb..ff4ab1ed9a 100644 --- a/vector/src/main/res/layout/view_url_preview.xml +++ b/vector/src/main/res/layout/view_url_preview.xml @@ -9,6 +9,7 @@ <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" + android:minWidth="@dimen/chat_bubble_fixed_size" android:orientation="vertical"> <ImageView @@ -17,6 +18,7 @@ android:layout_height="wrap_content" android:adjustViewBounds="true" android:importantForAccessibility="no" + android:layout_gravity="center" android:maxHeight="200dp" android:scaleType="fitXY" tools:src="@tools:sample/backgrounds/scenic" /> From 5ee4984ec843e073630f659f2d54f29e86110e03 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Tue, 18 Jan 2022 19:27:12 +0100 Subject: [PATCH 14/36] Bubbles: handle images and make small refactoring --- .../ui-styles/src/main/res/values/dimens.xml | 1 + .../src/main/res/values/styles_timeline.xml | 6 - .../timeline/item/AbsBaseMessageItem.kt | 7 +- .../timeline/item/MessageImageVideoItem.kt | 24 ++- .../timeline/style/TimelineMessageLayout.kt | 3 +- .../style/TimelineMessageLayoutFactory.kt | 8 + .../detail/timeline/view/MessageBubbleView.kt | 148 +++++++++--------- .../view/TimelineMessageLayoutRenderer.kt | 11 +- .../features/media/ImageContentRenderer.kt | 6 +- .../res/drawable/overlay_bubble_media.xml | 1 + ...em_timeline_event_view_stubs_container.xml | 25 +-- .../main/res/layout/view_message_bubble.xml | 4 +- 12 files changed, 131 insertions(+), 113 deletions(-) diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index d89b3f477f..15e2fc2fbb 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -54,6 +54,7 @@ <dimen name="chat_bubble_margin_start">28dp</dimen> <dimen name="chat_bubble_margin_end">62dp</dimen> <dimen name="chat_bubble_fixed_size">300dp</dimen> + <dimen name="chat_bubble_corner_radius">12dp</dimen> <!-- Onboarding --> <item name="ftue_auth_gutter_start_percent" format="float" type="dimen">0.05</item> diff --git a/library/ui-styles/src/main/res/values/styles_timeline.xml b/library/ui-styles/src/main/res/values/styles_timeline.xml index ef2f694d3e..127d9f3dc7 100644 --- a/library/ui-styles/src/main/res/values/styles_timeline.xml +++ b/library/ui-styles/src/main/res/values/styles_timeline.xml @@ -4,12 +4,6 @@ <style name="TimelineContentStubBaseParams"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">wrap_content</item> - <item name="android:layout_marginStart">8dp</item> - <item name="android:layout_marginLeft">8dp</item> - <item name="android:layout_marginEnd">8dp</item> - <item name="android:layout_marginRight">8dp</item> - <item name="android:layout_marginBottom">4dp</item> - <item name="android:layout_marginTop">4dp</item> </style> <style name="TimelineContentMediaPillStyle"> 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 3d9db4e827..ac9ef1522d 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,7 +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.home.room.detail.timeline.view.TimelineMessageLayoutRenderer 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 @@ -99,10 +99,7 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem holder.view.onClick(baseAttributes.itemClickListener) holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener) - (holder.view as? MessageViewConfiguration)?.apply { - isFirstFromSender = baseAttributes.informationData.isFirstFromThisSender - isLastFromSender = baseAttributes.informationData.isLastFromThisSender - } + (holder.view as? TimelineMessageLayoutRenderer)?.render(baseAttributes.informationData.messageLayout) } 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 799cb1a002..c9876da2f2 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 @@ -23,14 +23,17 @@ import androidx.core.view.ViewCompat import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners +import com.bumptech.glide.load.resource.bitmap.RoundedCorners import im.vector.app.R import im.vector.app.core.epoxy.ClickListener 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.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.view.MessageViewConfiguration +import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer import im.vector.app.features.media.ImageContentRenderer @EpoxyModelClass(layout = R.layout.item_timeline_event_base) @@ -56,7 +59,21 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold override fun bind(holder: Holder) { super.bind(holder) - imageContentRenderer.render(mediaData, mode, holder.imageView) + val messageLayout = baseAttributes.informationData.messageLayout + val dimensionConverter = DimensionConverter(holder.view.resources) + val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) { + val cornerRadius = holder.view.resources.getDimensionPixelSize(R.dimen.chat_bubble_corner_radius).toFloat() + val topRadius = if (messageLayout.isFirstFromThisSender) cornerRadius else 0f + val bottomRadius = if (messageLayout.isLastFromThisSender) cornerRadius else 0f + if (messageLayout.isIncoming) { + GranularRoundedCorners(topRadius, cornerRadius, cornerRadius, bottomRadius) + } else { + GranularRoundedCorners(cornerRadius, topRadius, bottomRadius, cornerRadius) + } + } else { + RoundedCorners(dimensionConverter.dpToPx(8)) + } + imageContentRenderer.render(mediaData, mode, holder.imageView, imageCornerTransformation) if (!attributes.informationData.sendState.hasFailed()) { contentUploadStateTrackerBinder.bind( attributes.informationData.eventId, @@ -72,8 +89,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold holder.mediaContentView.onClick(attributes.itemClickListener) holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener) holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE - (holder.view as? MessageViewConfiguration)?.showTimeAsOverlay = true - holder.overlayView.isVisible = baseAttributes.informationData.messageLayout is TimelineMessageLayout.Bubble + holder.overlayView.isVisible = messageLayout is TimelineMessageLayout.Bubble } override fun unbind(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt index bd4be10783..5d487c5d23 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt @@ -41,10 +41,11 @@ sealed interface TimelineMessageLayout : Parcelable { val isIncoming: Boolean, val isFirstFromThisSender: Boolean, val isLastFromThisSender: Boolean, + val isPseudoBubble: Boolean, override val layoutRes: Int = if (isIncoming) { R.layout.item_timeline_event_bubble_incoming_base } else { R.layout.item_timeline_event_bubble_outgoing_base - }, + } ) : TimelineMessageLayout } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index f5ca97dc1d..685f66f40e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -33,15 +33,22 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess private val vectorPreferences: VectorPreferences) { companion object { + // Can't be rendered in bubbles, so get back to default layout private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf( EventType.MESSAGE, EventType.POLL_START, EventType.ENCRYPTED, EventType.STICKER ) + // Can't be rendered in bubbles, so get back to default layout private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf( MessageType.MSGTYPE_VERIFICATION_REQUEST ) + + // Use the bubble layout but without borders + private val MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT = setOf( + MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_VIDEO, + ) } fun create(params: TimelineItemFactoryParams): TimelineMessageLayout { @@ -86,6 +93,7 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess isIncoming = !isSentByMe, isFirstFromThisSender = isFirstFromThisSender, isLastFromThisSender = isLastFromThisSender, + isPseudoBubble = messageContent?.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT ) } } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 0a3746f8a4..4731b76934 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -18,13 +18,12 @@ package im.vector.app.features.home.room.detail.timeline.view import android.content.Context import android.content.res.ColorStateList +import android.graphics.Color import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.View import android.view.ViewOutlineProvider import android.widget.RelativeLayout -import android.widget.TextView -import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes @@ -35,70 +34,38 @@ import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import im.vector.app.R import im.vector.app.core.utils.DimensionConverter +import im.vector.app.databinding.ViewMessageBubbleBinding +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.themes.ThemeUtils +import timber.log.Timber class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : - RelativeLayout(context, attrs, defStyleAttr), MessageViewConfiguration { + RelativeLayout(context, attrs, defStyleAttr), TimelineMessageLayoutRenderer { - override var isIncoming: Boolean = false - set(value) { - field = value - render() - } + private var isIncoming: Boolean = false - override var isFirstFromSender: Boolean = false - set(value) { - field = value - render() - } - override var isLastFromSender: Boolean = false - set(value) { - field = value - render() - } + private val cornerRadius = resources.getDimensionPixelSize(R.dimen.chat_bubble_corner_radius).toFloat() + private val horizontalStubPadding = DimensionConverter(resources).dpToPx(12) + private val verticalStubPadding = DimensionConverter(resources).dpToPx(4) - override var showTimeAsOverlay: Boolean = false - set(value) { - field = value - render() - } - - private val cornerRadius = DimensionConverter(resources).dpToPx(12).toFloat() + private lateinit var views: ViewMessageBubbleBinding init { inflate(context, R.layout.view_message_bubble, this) context.withStyledAttributes(attrs, R.styleable.MessageBubble) { isIncoming = getBoolean(R.styleable.MessageBubble_incoming_style, false) - showTimeAsOverlay = getBoolean(R.styleable.MessageBubble_show_time_overlay, false) - isFirstFromSender = getBoolean(R.styleable.MessageBubble_is_first, false) - isLastFromSender = getBoolean(R.styleable.MessageBubble_is_last, false) } } override fun onFinishInflate() { super.onFinishInflate() - render() - } - - private fun render() { + views = ViewMessageBubbleBinding.bind(this) val currentLayoutDirection = layoutDirection - val bubbleView: ConstraintLayout = findViewById(R.id.bubbleView) - bubbleView.apply { - background = createBackgroundDrawable() - outlineProvider = ViewOutlineProvider.BACKGROUND - clipToOutline = true - } if (isIncoming) { - findViewById<View>(R.id.informationBottom).layoutDirection = currentLayoutDirection - findViewById<View>(R.id.bubbleWrapper).layoutDirection = currentLayoutDirection - bubbleView.layoutDirection = currentLayoutDirection - findViewById<View>(R.id.messageEndGuideline).updateLayoutParams<LayoutParams> { - marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end) - } - findViewById<View>(R.id.messageStartGuideline).updateLayoutParams<LayoutParams> { - marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start) - } + views.informationBottom.layoutDirection = currentLayoutDirection + views.bubbleWrapper.layoutDirection = currentLayoutDirection + views.bubbleView.layoutDirection = currentLayoutDirection } else { val oppositeLayoutDirection = if (currentLayoutDirection == View.LAYOUT_DIRECTION_LTR) { View.LAYOUT_DIRECTION_RTL @@ -106,41 +73,66 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri View.LAYOUT_DIRECTION_LTR } - findViewById<View>(R.id.informationBottom).layoutDirection = oppositeLayoutDirection - findViewById<View>(R.id.bubbleWrapper).layoutDirection = oppositeLayoutDirection - bubbleView.layoutDirection = currentLayoutDirection - findViewById<View>(R.id.messageEndGuideline).updateLayoutParams<LayoutParams> { - marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start) - } - findViewById<View>(R.id.messageStartGuideline).updateLayoutParams<LayoutParams> { - marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end) - } - } - ConstraintSet().apply { - clone(bubbleView) - clear(R.id.viewStubContainer, ConstraintSet.END) - if (showTimeAsOverlay) { - val timeColor = ContextCompat.getColor(context, R.color.palette_white) - findViewById<TextView>(R.id.messageTimeView).setTextColor(timeColor) - connect(R.id.viewStubContainer, ConstraintSet.END, R.id.parent, ConstraintSet.END, 0) - val margin = resources.getDimensionPixelSize(R.dimen.layout_horizontal_margin) - setMargin(R.id.messageTimeView, ConstraintSet.END, margin) - } else { - val timeColor = ThemeUtils.getColor(context, R.attr.vctr_content_tertiary) - findViewById<TextView>(R.id.messageTimeView).setTextColor(timeColor) - connect(R.id.viewStubContainer, ConstraintSet.END, R.id.messageTimeView, ConstraintSet.START, 0) - } - applyTo(bubbleView) + views.informationBottom.layoutDirection = oppositeLayoutDirection + views.bubbleWrapper.layoutDirection = oppositeLayoutDirection + views.bubbleView.layoutDirection = currentLayoutDirection } } - private fun createBackgroundDrawable(): Drawable { - val (topCornerFamily, topRadius) = if (isFirstFromSender) { + override fun render(messageLayout: TimelineMessageLayout) { + if (messageLayout !is TimelineMessageLayout.Bubble) { + Timber.v("Can't render messageLayout $messageLayout") + return + } + views.bubbleView.apply { + background = createBackgroundDrawable(messageLayout) + outlineProvider = ViewOutlineProvider.BACKGROUND + clipToOutline = true + } + ConstraintSet().apply { + clone(views.bubbleView) + clear(R.id.viewStubContainer, ConstraintSet.END) + val showTimeAsOverlay = messageLayout.isPseudoBubble + if (showTimeAsOverlay) { + val timeColor = ContextCompat.getColor(context, R.color.palette_white) + views.messageTimeView.setTextColor(timeColor) + connect(R.id.viewStubContainer, ConstraintSet.END, R.id.parent, ConstraintSet.END, 0) + } else { + val timeColor = ThemeUtils.getColor(context, R.attr.vctr_content_tertiary) + views.messageTimeView.setTextColor(timeColor) + connect(R.id.viewStubContainer, ConstraintSet.END, R.id.messageTimeView, ConstraintSet.START, 0) + } + applyTo(views.bubbleView) + } + if (messageLayout.isPseudoBubble) { + views.viewStubContainer.root.setPadding(0, 0, 0, 0) + } else { + views.viewStubContainer.root.setPadding(horizontalStubPadding, verticalStubPadding, horizontalStubPadding, verticalStubPadding) + } + if (messageLayout.isIncoming) { + views.messageEndGuideline.updateLayoutParams<LayoutParams> { + marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end) + } + views.messageStartGuideline.updateLayoutParams<LayoutParams> { + marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start) + } + } else { + views.messageEndGuideline.updateLayoutParams<LayoutParams> { + marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start) + } + views.messageStartGuideline.updateLayoutParams<LayoutParams> { + marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end) + } + } + } + + private fun createBackgroundDrawable(messageLayout: TimelineMessageLayout.Bubble): Drawable { + val (topCornerFamily, topRadius) = if (messageLayout.isFirstFromThisSender) { Pair(CornerFamily.ROUNDED, cornerRadius) } else { Pair(CornerFamily.CUT, 0f) } - val (bottomCornerFamily, bottomRadius) = if (isLastFromSender) { + val (bottomCornerFamily, bottomRadius) = if (messageLayout.isLastFromThisSender) { Pair(CornerFamily.ROUNDED, cornerRadius) } else { Pair(CornerFamily.CUT, 0f) @@ -166,7 +158,11 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } val shapeAppearanceModel = shapeAppearanceModelBuilder.build() return MaterialShapeDrawable(shapeAppearanceModel).apply { - fillColor = ColorStateList.valueOf(backgroundColor) + fillColor = if (messageLayout.isPseudoBubble) { + ColorStateList.valueOf(Color.TRANSPARENT) + } else { + ColorStateList.valueOf(backgroundColor) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/TimelineMessageLayoutRenderer.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/TimelineMessageLayoutRenderer.kt index 23dd9860f8..a339119f46 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/TimelineMessageLayoutRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/TimelineMessageLayoutRenderer.kt @@ -16,11 +16,8 @@ package im.vector.app.features.home.room.detail.timeline.view -interface MessageViewConfiguration { - var isIncoming: Boolean - var isFirstFromSender: Boolean - var isLastFromSender: Boolean - var showTimeAsOverlay: Boolean - var showNoBubble: Boolean - fun render() +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout + +interface TimelineMessageLayoutRenderer { + fun render(messageLayout: TimelineMessageLayout) } diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index 8d6d1f467b..be9d24272d 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -16,6 +16,7 @@ package im.vector.app.features.media +import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.Uri import android.os.Parcelable @@ -23,6 +24,7 @@ import android.view.View import android.widget.ImageView import androidx.core.view.updateLayoutParams import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Transformation import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.load.resource.bitmap.RoundedCorners @@ -109,7 +111,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc .into(imageView) } - fun render(data: Data, mode: Mode, imageView: ImageView) { + fun render(data: Data, mode: Mode, imageView: ImageView, cornerTransformation: Transformation<Bitmap> = RoundedCorners(dimensionConverter.dpToPx(8))) { val size = processSize(data, mode) imageView.updateLayoutParams { width = size.width @@ -120,7 +122,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc createGlideRequest(data, mode, imageView, size) .dontAnimate() - .transform(RoundedCorners(dimensionConverter.dpToPx(8))) + .transform(cornerTransformation) // .thumbnail(0.3f) .into(imageView) } diff --git a/vector/src/main/res/drawable/overlay_bubble_media.xml b/vector/src/main/res/drawable/overlay_bubble_media.xml index ce34a39037..8bc9c58064 100644 --- a/vector/src/main/res/drawable/overlay_bubble_media.xml +++ b/vector/src/main/res/drawable/overlay_bubble_media.xml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="@dimen/chat_bubble_corner_radius"/> <gradient android:type="linear" android:angle="270" diff --git a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml index 576b2d4f01..440a1f4602 100644 --- a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml +++ b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml @@ -3,18 +3,24 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:addStatesFromChildren="true"> + android:addStatesFromChildren="true" + android:paddingStart="8dp" + android:paddingLeft="8dp" + android:paddingTop="4dp" + android:paddingEnd="8dp" + android:paddingRight="8dp" + android:paddingBottom="4dp"> <ViewStub android:id="@+id/messageContentTextStub" - style="@style/TimelineContentStubBaseParams" + android:layout_width="match_parent" android:layout_height="wrap_content" android:layout="@layout/item_timeline_event_text_message_stub" tools:visibility="visible" /> <ViewStub android:id="@+id/messageContentCodeBlockStub" - style="@style/TimelineContentStubBaseParams" + android:layout_width="match_parent" android:layout_height="wrap_content" android:layout="@layout/item_timeline_event_code_block_stub" tools:visibility="visible" /> @@ -22,33 +28,34 @@ <ViewStub android:id="@+id/messageContentMediaStub" style="@style/TimelineContentStubBaseParams" + android:layout_width="match_parent" android:layout_height="wrap_content" android:inflatedId="@+id/messageContentMedia" android:layout="@layout/item_timeline_event_media_message_stub" /> <ViewStub android:id="@+id/messageContentFileStub" - style="@style/TimelineContentStubBaseParams" + android:layout_width="match_parent" android:layout_height="wrap_content" - tools:visibility="gone" - android:layout="@layout/item_timeline_event_file_stub" /> + android:layout="@layout/item_timeline_event_file_stub" + tools:visibility="gone" /> <ViewStub android:id="@+id/messageContentRedactedStub" - style="@style/TimelineContentStubBaseParams" + android:layout_width="match_parent" android:layout_height="wrap_content" android:layout="@layout/item_timeline_event_redacted_stub" /> <ViewStub android:id="@+id/messageContentVoiceStub" - style="@style/TimelineContentStubBaseParams" + android:layout_width="match_parent" android:layout_height="wrap_content" android:layout="@layout/item_timeline_event_voice_stub" tools:visibility="gone" /> <ViewStub android:id="@+id/messageContentPollStub" - style="@style/TimelineContentStubBaseParams" + android:layout_width="match_parent" android:layout_height="wrap_content" android:layout="@layout/item_timeline_event_poll" /> diff --git a/vector/src/main/res/layout/view_message_bubble.xml b/vector/src/main/res/layout/view_message_bubble.xml index 72e8f7dc29..d6263b96d7 100644 --- a/vector/src/main/res/layout/view_message_bubble.xml +++ b/vector/src/main/res/layout/view_message_bubble.xml @@ -4,8 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - tools:parentTag="android.widget.RelativeLayout" - tools:viewBindingIgnore="true"> + tools:parentTag="android.widget.RelativeLayout"> <im.vector.app.core.platform.CheckableView android:id="@+id/messageSelectedBackground" @@ -91,7 +90,6 @@ android:layout_marginStart="0dp" android:layout_marginEnd="0dp" android:addStatesFromChildren="true" - android:paddingStart="4dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> From ac0c2624f0818ea0c57d472272950cc1a7bf861a Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Wed, 19 Jan 2022 11:49:33 +0100 Subject: [PATCH 15/36] Bubbles: update sticker handling --- .../room/detail/timeline/item/MessageImageVideoItem.kt | 2 +- .../room/detail/timeline/style/TimelineMessageLayout.kt | 1 + .../timeline/style/TimelineMessageLayoutFactory.kt | 9 +++++++-- .../home/room/detail/timeline/view/MessageBubbleView.kt | 3 +-- 4 files changed, 10 insertions(+), 5 deletions(-) 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 c9876da2f2..274841973e 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 @@ -89,7 +89,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold holder.mediaContentView.onClick(attributes.itemClickListener) holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener) holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE - holder.overlayView.isVisible = messageLayout is TimelineMessageLayout.Bubble + holder.overlayView.isVisible = messageLayout is TimelineMessageLayout.Bubble && messageLayout.timestampAsOverlay } override fun unbind(holder: Holder) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt index 5d487c5d23..bdcbc52037 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt @@ -42,6 +42,7 @@ sealed interface TimelineMessageLayout : Parcelable { val isFirstFromThisSender: Boolean, val isLastFromThisSender: Boolean, val isPseudoBubble: Boolean, + val timestampAsOverlay: Boolean, override val layoutRes: Int = if (isIncoming) { R.layout.item_timeline_event_bubble_incoming_base } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index 685f66f40e..e12e27472c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -40,6 +40,7 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess EventType.ENCRYPTED, EventType.STICKER ) + // Can't be rendered in bubbles, so get back to default layout private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf( MessageType.MSGTYPE_VERIFICATION_REQUEST @@ -47,7 +48,10 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess // Use the bubble layout but without borders private val MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT = setOf( - MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_STICKER_LOCAL + ) + private val MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY = setOf( + MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_VIDEO ) } @@ -93,7 +97,8 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess isIncoming = !isSentByMe, isFirstFromThisSender = isFirstFromThisSender, isLastFromThisSender = isLastFromThisSender, - isPseudoBubble = messageContent?.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT + isPseudoBubble = messageContent?.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT, + timestampAsOverlay = messageContent?.msgType in MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY ) } } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 4731b76934..d36e3068b7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -92,8 +92,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri ConstraintSet().apply { clone(views.bubbleView) clear(R.id.viewStubContainer, ConstraintSet.END) - val showTimeAsOverlay = messageLayout.isPseudoBubble - if (showTimeAsOverlay) { + if (messageLayout.timestampAsOverlay) { val timeColor = ContextCompat.getColor(context, R.color.palette_white) views.messageTimeView.setTextColor(timeColor) connect(R.id.viewStubContainer, ConstraintSet.END, R.id.parent, ConstraintSet.END, 0) From 2d9454c5b655f85b3841b2b21a52879c49a291f4 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Wed, 19 Jan 2022 16:19:47 +0100 Subject: [PATCH 16/36] Bubbles: first iteration on url preview --- .../home/room/detail/timeline/item/MessageTextItem.kt | 9 +++++++++ .../home/room/detail/timeline/url/PreviewUrlView.kt | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index 12e5d6a8dd..60fe1a4b2c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -16,6 +16,7 @@ package im.vector.app.features.home.room.detail.timeline.item +import android.graphics.Color import android.text.Spanned import android.text.method.MovementMethod import androidx.appcompat.widget.AppCompatTextView @@ -29,11 +30,13 @@ import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onLongClickIgnoringLinks 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.findPillsAndProcess 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.media.ImageContentRenderer +import im.vector.app.features.themes.ThemeUtils import io.noties.markwon.MarkwonPlugin import org.matrix.android.sdk.api.extensions.orFalse @@ -80,6 +83,12 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() { safePreviewUrlRetriever.addListener(attributes.informationData.eventId, previewUrlViewUpdater) } holder.previewUrlView.delegate = previewUrlCallback + val urlPreviewBackgroundColor = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) { + Color.TRANSPARENT + } else { + ThemeUtils.getColor(holder.view.context, R.attr.vctr_system) + } + holder.previewUrlView.setCardBackgroundColor(urlPreviewBackgroundColor) if (useBigFont) { holder.messageView.textSize = 44F diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt index 631f00819c..e41f62c373 100755 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt @@ -47,7 +47,6 @@ class PreviewUrlView @JvmOverloads constructor( setupView() radius = resources.getDimensionPixelSize(R.dimen.preview_url_view_corner_radius).toFloat() cardElevation = 0f - setCardBackgroundColor(ThemeUtils.getColor(context, R.attr.vctr_system)) } private var state: PreviewUrlUiState = PreviewUrlUiState.Unknown From 8c4dff4db9f8443efecda3d6c82cf5e33cca310d Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Thu, 20 Jan 2022 15:13:54 +0100 Subject: [PATCH 17/36] Bubbles: change again url preview --- .../ui-styles/src/main/res/values/dimens.xml | 1 + .../sdk/api/session/media/PreviewUrlData.kt | 6 +- .../database/RealmSessionStoreMigration.kt | 14 +++- .../database/model/PreviewUrlCacheEntity.kt | 3 +- .../session/media/GetPreviewUrlTask.kt | 11 ++- .../session/media/PreviewUrlMapper.kt | 4 +- .../detail/timeline/item/MessageTextItem.kt | 7 +- .../detail/timeline/url/PreviewUrlView.kt | 26 ++++++- .../features/media/ImageContentRenderer.kt | 18 ++++- .../item_timeline_event_text_message_stub.xml | 5 +- .../src/main/res/layout/view_url_preview.xml | 77 ++++++++++++------- 11 files changed, 123 insertions(+), 49 deletions(-) diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index 15e2fc2fbb..04ed7fcc55 100644 --- a/library/ui-styles/src/main/res/values/dimens.xml +++ b/library/ui-styles/src/main/res/values/dimens.xml @@ -44,6 +44,7 @@ <!-- Preview Url --> <dimen name="preview_url_view_corner_radius">8dp</dimen> + <dimen name="preview_url_view_image_max_height">160dp</dimen> <!-- Composer --> <dimen name="composer_min_height">56dp</dimen> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt index 33fc8b052b..bfba43a82d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/media/PreviewUrlData.kt @@ -47,5 +47,9 @@ data class PreviewUrlData( // Value of field "og:description" val description: String?, // Value of field "og:image" - val mxcUrl: String? + val mxcUrl: String?, + // Value of field "og:image:width" + val imageWidth: Int?, + // Value of field "og:image:height" + val imageHeight: Int? ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 1f45ac2a75..7a2511feac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -56,7 +56,7 @@ internal class RealmSessionStoreMigration @Inject constructor( ) : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 21L + const val SESSION_STORE_SCHEMA_VERSION = 22L } /** @@ -90,6 +90,7 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion <= 18) migrateTo19(realm) if (oldVersion <= 19) migrateTo20(realm) if (oldVersion <= 20) migrateTo21(realm) + if (oldVersion <= 21) migrateTo22(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -445,4 +446,15 @@ internal class RealmSessionStoreMigration @Inject constructor( } } } + + private fun migrateTo22(realm: DynamicRealm) { + Timber.d("Step 21 -> 22") + + realm.schema.get("PreviewUrlCacheEntity") + ?.addField(PreviewUrlCacheEntityFields.IMAGE_WIDTH, Int::class.java) + ?.setNullable(PreviewUrlCacheEntityFields.IMAGE_WIDTH, true) + ?.addField(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, Int::class.java) + ?.setNullable(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, true) + } + } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt index b1e0b64405..f19d70a1f2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PreviewUrlCacheEntity.kt @@ -28,7 +28,8 @@ internal open class PreviewUrlCacheEntity( var title: String? = null, var description: String? = null, var mxcUrl: String? = null, - + var imageWidth: Int? = null, + var imageHeight: Int? = null, var lastUpdatedTimestamp: Long = 0L ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt index e707c2351c..32bcf3f7ca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/GetPreviewUrlTask.kt @@ -48,8 +48,8 @@ internal class DefaultGetPreviewUrlTask @Inject constructor( override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData { return when (params.cacheStrategy) { - CacheStrategy.NoCache -> doRequest(params.url, params.timestamp) - is CacheStrategy.TtlCache -> doRequestWithCache( + CacheStrategy.NoCache -> doRequest(params.url, params.timestamp) + is CacheStrategy.TtlCache -> doRequestWithCache( params.url, params.timestamp, params.cacheStrategy.validityDurationInMillis, @@ -77,7 +77,9 @@ internal class DefaultGetPreviewUrlTask @Inject constructor( siteName = (get("og:site_name") as? String)?.unescapeHtml(), title = (get("og:title") as? String)?.unescapeHtml(), description = (get("og:description") as? String)?.unescapeHtml(), - mxcUrl = get("og:image") as? String + mxcUrl = get("og:image") as? String, + imageHeight = (get("og:image:height") as? Double)?.toInt(), + imageWidth = (get("og:image:width") as? Double)?.toInt(), ) } @@ -114,7 +116,8 @@ internal class DefaultGetPreviewUrlTask @Inject constructor( previewUrlCacheEntity.title = data.title previewUrlCacheEntity.description = data.description previewUrlCacheEntity.mxcUrl = data.mxcUrl - + previewUrlCacheEntity.imageHeight = data.imageHeight + previewUrlCacheEntity.imageWidth = data.imageWidth previewUrlCacheEntity.lastUpdatedTimestamp = Date().time } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt index dd1a9ead26..551dc29b92 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/media/PreviewUrlMapper.kt @@ -27,5 +27,7 @@ internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData( siteName = siteName, title = title, description = description, - mxcUrl = mxcUrl + mxcUrl = mxcUrl, + imageWidth = imageWidth, + imageHeight = imageHeight ) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index 60fe1a4b2c..157749ab74 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -83,12 +83,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() { safePreviewUrlRetriever.addListener(attributes.informationData.eventId, previewUrlViewUpdater) } holder.previewUrlView.delegate = previewUrlCallback - val urlPreviewBackgroundColor = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) { - Color.TRANSPARENT - } else { - ThemeUtils.getColor(holder.view.context, R.attr.vctr_system) - } - holder.previewUrlView.setCardBackgroundColor(urlPreviewBackgroundColor) + holder.previewUrlView.render(attributes.informationData.messageLayout) if (useBigFont) { holder.messageView.textSize = 44F diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt index e41f62c373..905d420463 100755 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt @@ -17,14 +17,20 @@ package im.vector.app.features.home.room.detail.timeline.url import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color import android.util.AttributeSet import android.view.View import androidx.core.view.isVisible import com.google.android.material.card.MaterialCardView import im.vector.app.R import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.glide.GlideApp +import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.ViewUrlPreviewBinding 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.view.TimelineMessageLayoutRenderer import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.themes.ThemeUtils import org.matrix.android.sdk.api.extensions.orFalse @@ -37,7 +43,7 @@ class PreviewUrlView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : MaterialCardView(context, attrs, defStyleAttr), View.OnClickListener { +) : MaterialCardView(context, attrs, defStyleAttr), View.OnClickListener, TimelineMessageLayoutRenderer { private lateinit var views: ViewUrlPreviewBinding @@ -75,6 +81,22 @@ class PreviewUrlView @JvmOverloads constructor( } } + override fun render(messageLayout: TimelineMessageLayout) { + when (messageLayout) { + is TimelineMessageLayout.Default -> { + val backgroundColor = ThemeUtils.getColor(context, R.attr.vctr_system) + setCardBackgroundColor(backgroundColor) + val guidelineBegin = DimensionConverter(resources).dpToPx(8) + views.urlPreviewStartGuideline.setGuidelineBegin(guidelineBegin) + } + is TimelineMessageLayout.Bubble -> { + setCardBackgroundColor(Color.TRANSPARENT) + rippleColor = ColorStateList.valueOf(Color.TRANSPARENT) + views.urlPreviewStartGuideline.setGuidelineBegin(0) + } + } + } + override fun onClick(v: View?) { when (val finalState = state) { is PreviewUrlUiState.Data -> delegate?.onPreviewUrlClicked(finalState.url) @@ -126,7 +148,7 @@ class PreviewUrlView @JvmOverloads constructor( isVisible = true views.urlPreviewTitle.setTextOrHide(previewUrlData.title) - views.urlPreviewImage.isVisible = previewUrlData.mxcUrl?.let { imageContentRenderer.render(it, views.urlPreviewImage) }.orFalse() + views.urlPreviewImage.isVisible = imageContentRenderer.render(previewUrlData, views.urlPreviewImage) views.urlPreviewDescription.setTextOrHide(previewUrlData.description) views.urlPreviewDescription.maxLines = when { previewUrlData.mxcUrl != null -> 2 diff --git a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt index be9d24272d..65c99362b9 100644 --- a/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/media/ImageContentRenderer.kt @@ -44,6 +44,7 @@ import im.vector.app.core.utils.DimensionConverter import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.media.PreviewUrlData import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt import timber.log.Timber import java.io.File @@ -61,6 +62,9 @@ interface AttachmentData : Parcelable { val allowNonMxcUrls: Boolean } +private const val URL_PREVIEW_IMAGE_MIN_FULL_WIDTH_PX = 600 +private const val URL_PREVIEW_IMAGE_MIN_FULL_HEIGHT_PX = 315 + class ImageContentRenderer @Inject constructor(private val localFilesHelper: LocalFilesHelper, private val activeSessionHolder: ActiveSessionHolder, private val dimensionConverter: DimensionConverter) { @@ -89,12 +93,20 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc /** * For url preview */ - fun render(mxcUrl: String, imageView: ImageView): Boolean { + fun render(previewUrlData: PreviewUrlData, imageView: ImageView): Boolean { val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver() - val imageUrl = contentUrlResolver.resolveFullSize(mxcUrl) ?: return false - + val imageUrl = contentUrlResolver.resolveFullSize(previewUrlData.mxcUrl) ?: return false + val maxHeight = dimensionConverter.resources.getDimensionPixelSize(R.dimen.preview_url_view_image_max_height) + val height = previewUrlData.imageHeight ?: URL_PREVIEW_IMAGE_MIN_FULL_HEIGHT_PX + val width = previewUrlData.imageWidth ?: URL_PREVIEW_IMAGE_MIN_FULL_WIDTH_PX + if (height < URL_PREVIEW_IMAGE_MIN_FULL_HEIGHT_PX || width < URL_PREVIEW_IMAGE_MIN_FULL_WIDTH_PX) { + imageView.scaleType = ImageView.ScaleType.CENTER_INSIDE + } else { + imageView.scaleType = ImageView.ScaleType.CENTER_CROP + } GlideApp.with(imageView) .load(imageUrl) + .override(width, height.coerceAtMost(maxHeight)) .into(imageView) return true } diff --git a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml index 4bb612fedf..e2a11c7926 100644 --- a/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_text_message_stub.xml @@ -17,12 +17,11 @@ <im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView android:id="@+id/messageUrlPreview" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:layout_marginBottom="4dp" - android:foreground="?attr/selectableItemBackground" android:visibility="gone" - tools:visibility="gone" /> + tools:visibility="visible" /> </LinearLayout> diff --git a/vector/src/main/res/layout/view_url_preview.xml b/vector/src/main/res/layout/view_url_preview.xml index ff4ab1ed9a..e9774207ca 100644 --- a/vector/src/main/res/layout/view_url_preview.xml +++ b/vector/src/main/res/layout/view_url_preview.xml @@ -1,79 +1,102 @@ <?xml version="1.0" encoding="utf-8"?> <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/informationUrlPreviewContainer" - android:layout_width="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" tools:parentTag="com.google.android.material.card.MaterialCardView"> - <LinearLayout - android:layout_width="wrap_content" + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" android:layout_height="wrap_content" - android:minWidth="@dimen/chat_bubble_fixed_size" + android:minWidth="208dp" android:orientation="vertical"> + <!--Image dimensions will be overrode by ImageContentRenderer --> <ImageView android:id="@+id/url_preview_image" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintHeight_max="208dp" + android:scaleType="centerCrop" + android:importantForAccessibility="no" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toEndOf="parent" + tools:src="@tools:sample/backgrounds/scenic" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/url_preview_start_guideline" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:adjustViewBounds="true" - android:importantForAccessibility="no" - android:layout_gravity="center" - android:maxHeight="200dp" - android:scaleType="fitXY" - tools:src="@tools:sample/backgrounds/scenic" /> + android:orientation="vertical" + app:layout_constraintGuide_begin="8dp" /> <TextView android:id="@+id/url_preview_site" style="@style/Widget.Vector.TextView.Caption" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" android:layout_marginTop="6dp" android:layout_marginEnd="8dp" android:ellipsize="end" android:maxLines="1" android:singleLine="true" + app:layout_constraintEnd_toStartOf="@id/url_preview_close" android:textColor="?vctr_content_secondary" + app:layout_constraintStart_toStartOf="@id/url_preview_start_guideline" + app:layout_constraintTop_toBottomOf="@id/url_preview_image" + app:layout_goneMarginTop="12dp" tools:text="BBC News" /> <TextView android:id="@+id/url_preview_title" style="@style/Widget.Vector.TextView.Body.Medium" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" android:layout_marginTop="4dp" - android:layout_marginEnd="@dimen/layout_touch_size" + android:layout_marginEnd="8dp" android:ellipsize="end" android:maxLines="2" android:textColor="?vctr_content_primary" android:textStyle="bold" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/url_preview_close" + app:layout_constraintStart_toStartOf="@id/url_preview_start_guideline" + app:layout_constraintTop_toBottomOf="@id/url_preview_site" + app:layout_goneMarginTop="12dp" tools:text="Jo Malone denounces her former brand's John Boyega decision" /> <TextView android:id="@+id/url_preview_description" style="@style/Widget.Vector.TextView.Body" - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" android:layout_marginTop="4dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" android:ellipsize="end" android:textColor="?vctr_content_secondary" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/url_preview_start_guideline" + app:layout_constraintTop_toBottomOf="@id/url_preview_title" tools:text="The British perfumer says removing actor John Boyega from his own advert was “utterly despicable”." /> - </LinearLayout> + <ImageView + android:id="@+id/url_preview_close" + android:layout_width="@dimen/layout_touch_size" + android:layout_height="@dimen/layout_touch_size" + android:layout_gravity="top|end" + android:contentDescription="@string/action_close" + android:scaleType="center" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + android:src="@drawable/ic_close_with_circular_bg" + tools:ignore="MissingPrefix" /> + + </androidx.constraintlayout.widget.ConstraintLayout> - <ImageView - android:id="@+id/url_preview_close" - android:layout_width="@dimen/layout_touch_size" - android:layout_height="@dimen/layout_touch_size" - android:layout_gravity="top|end" - android:contentDescription="@string/action_close" - android:scaleType="center" - android:src="@drawable/ic_close_with_circular_bg" - tools:ignore="MissingPrefix" /> </merge> \ No newline at end of file From 713f6f7a59ed4e0aa8204f227cb7785a5a628681 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Fri, 21 Jan 2022 16:01:13 +0100 Subject: [PATCH 18/36] Timeline: Add spacing to quote --- .../app/features/html/EventHtmlRenderer.kt | 13 +++-- .../app/features/html/ParagraphHandler.kt | 48 +++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index 36acad8854..5fc1306b42 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -17,9 +17,11 @@ package im.vector.app.features.html import android.content.Context +import android.content.res.Resources import android.text.Spannable import androidx.core.text.toSpannable import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.settings.VectorPreferences import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.Markwon @@ -53,11 +55,11 @@ class EventHtmlRenderer @Inject constructor( .usePlugin(object : AbstractMarkwonPlugin() { // Markwon expects maths to be in a specific format: https://noties.io/Markwon/docs/v4/ext-latex override fun processMarkdown(markdown: String): String { return markdown - .replace(Regex("""<span\s+data-mx-maths="([^"]*)">.*?</span>""")) { - matchResult -> "$$" + matchResult.groupValues[1] + "$$" + .replace(Regex("""<span\s+data-mx-maths="([^"]*)">.*?</span>""")) { matchResult -> + "$$" + matchResult.groupValues[1] + "$$" } - .replace(Regex("""<div\s+data-mx-maths="([^"]*)">.*?</div>""")) { - matchResult -> "\n$$\n" + matchResult.groupValues[1] + "\n$$\n" + .replace(Regex("""<div\s+data-mx-maths="([^"]*)">.*?</div>""")) { matchResult -> + "\n$$\n" + matchResult.groupValues[1] + "\n$$\n" } } }) @@ -112,11 +114,12 @@ class EventHtmlRenderer @Inject constructor( } } -class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider) : HtmlPlugin.HtmlConfigure { +class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider, private val resources: Resources) : HtmlPlugin.HtmlConfigure { override fun configureHtml(plugin: HtmlPlugin) { plugin .addHandler(FontTagHandler()) + .addHandler(ParagraphHandler(DimensionConverter(resources))) .addHandler(MxReplyTagHandler()) .addHandler(SpanHandler(colorProvider)) } diff --git a/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt b/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt new file mode 100644 index 0000000000..e62134ae7c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2019 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.html + +import android.content.res.Resources +import im.vector.app.core.utils.DimensionConverter +import io.noties.markwon.MarkwonVisitor +import io.noties.markwon.SpannableBuilder +import io.noties.markwon.html.HtmlTag +import io.noties.markwon.html.MarkwonHtmlRenderer +import io.noties.markwon.html.TagHandler +import me.gujun.android.span.style.VerticalPaddingSpan +import org.commonmark.node.BlockQuote + +class ParagraphHandler(private val dimensionConverter: DimensionConverter) : TagHandler() { + + override fun supportedTags() = listOf("p") + + override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { + if (tag.isBlock) { + visitChildren(visitor, renderer, tag.asBlock) + } + val configuration = visitor.configuration() + val factory = configuration.spansFactory().get(BlockQuote::class.java) + if (factory != null) { + SpannableBuilder.setSpans( + visitor.builder(), + VerticalPaddingSpan(dimensionConverter.dpToPx(16), 0), + tag.start(), + tag.end() + ) + } + } +} From 158026985772709cfcc5d93468bc9691cff2ff84 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Fri, 21 Jan 2022 16:01:54 +0100 Subject: [PATCH 19/36] Bubbles: move settings to timeline section --- vector/src/main/res/values/strings.xml | 2 +- .../src/main/res/xml/vector_settings_preferences.xml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 2e476f65d2..06c3b7f41e 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3705,7 +3705,7 @@ <string name="delete_poll_dialog_title">Remove poll</string> <string name="delete_poll_dialog_content">Are you sure you want to remove this poll? You won\'t be able to recover it once removed.</string> - <string name="message_bubbles">Message bubbles</string> + <string name="message_bubbles">Show Message bubbles</string> <string name="tooltip_attachment_photo">Open camera</string> <string name="tooltip_attachment_gallery">Send images and videos</string> diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index a25562e3d2..080a0a6bc2 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -22,11 +22,6 @@ android:title="@string/settings_theme" app:iconSpaceReserved="false" /> - <im.vector.app.core.preference.VectorSwitchPreference - android:defaultValue="false" - android:key="SETTINGS_INTERFACE_BUBBLE_KEY" - android:title="@string/message_bubbles" /> - <im.vector.app.core.preference.VectorPreference android:dialogTitle="@string/font_size" android:key="SETTINGS_INTERFACE_TEXT_SIZE_KEY" @@ -81,6 +76,11 @@ <im.vector.app.core.preference.VectorPreferenceCategory android:title="@string/settings_category_timeline"> + <im.vector.app.core.preference.VectorSwitchPreference + android:defaultValue="false" + android:key="SETTINGS_INTERFACE_BUBBLE_KEY" + android:title="@string/message_bubbles" /> + <im.vector.app.core.preference.VectorSwitchPreference android:defaultValue="true" android:key="SETTINGS_SHOW_URL_PREVIEW_KEY" From 608d8a5d54e82ee3e2402a88bf9e631a44528e3d Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Mon, 24 Jan 2022 19:31:04 +0100 Subject: [PATCH 20/36] Bubbles: change colors --- .../src/main/res/values/colors_message_bubble.xml | 11 +++++++++++ library/ui-styles/src/main/res/values/theme_dark.xml | 2 ++ library/ui-styles/src/main/res/values/theme_light.xml | 2 ++ .../room/detail/timeline/view/MessageBubbleView.kt | 8 ++++---- 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 library/ui-styles/src/main/res/values/colors_message_bubble.xml diff --git a/library/ui-styles/src/main/res/values/colors_message_bubble.xml b/library/ui-styles/src/main/res/values/colors_message_bubble.xml new file mode 100644 index 0000000000..7ac68574b6 --- /dev/null +++ b/library/ui-styles/src/main/res/values/colors_message_bubble.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <!-- Timeline bubble background colors --> + <attr name="vctr_message_bubble_inbound" format="color" /> + <color name="vctr_message_bubble_inbound_light">#E8EDF4</color> + <color name="vctr_message_bubble_inbound_dark">#21262C</color> + <attr name="vctr_message_bubble_outbound" format="color" /> + <color name="vctr_message_bubble_outbound_light">#E7F8F3</color> + <color name="vctr_message_bubble_outbound_dark">#133A34</color> +</resources> \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/theme_dark.xml b/library/ui-styles/src/main/res/values/theme_dark.xml index b828855721..542b2d4bbc 100644 --- a/library/ui-styles/src/main/res/values/theme_dark.xml +++ b/library/ui-styles/src/main/res/values/theme_dark.xml @@ -31,6 +31,8 @@ <item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_dark</item> <item name="vctr_chat_effect_snow_background">@color/vctr_chat_effect_snow_background_dark</item> <item name="vctr_toolbar_background">@color/element_system_dark</item> + <item name="vctr_message_bubble_inbound">@color/vctr_message_bubble_inbound_dark</item> + <item name="vctr_message_bubble_outbound">@color/vctr_message_bubble_outbound_dark</item> <!-- room message colors --> <item name="vctr_notice_secondary">#61708B</item> diff --git a/library/ui-styles/src/main/res/values/theme_light.xml b/library/ui-styles/src/main/res/values/theme_light.xml index 790a0bfc7c..5f032481d5 100644 --- a/library/ui-styles/src/main/res/values/theme_light.xml +++ b/library/ui-styles/src/main/res/values/theme_light.xml @@ -31,6 +31,8 @@ <item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_light</item> <item name="vctr_chat_effect_snow_background">@color/vctr_chat_effect_snow_background_light</item> <item name="vctr_toolbar_background">@color/element_background_light</item> + <item name="vctr_message_bubble_inbound">@color/vctr_message_bubble_inbound_light</item> + <item name="vctr_message_bubble_outbound">@color/vctr_message_bubble_outbound_light</item> <!-- room message colors --> <item name="vctr_notice_secondary">#61708B</item> diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index d36e3068b7..f1250f7efe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -139,16 +139,16 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri val shapeAppearanceModelBuilder = ShapeAppearanceModel().toBuilder() val backgroundColor: Int if (isIncoming) { - backgroundColor = ThemeUtils.getColor(context, R.attr.vctr_system) + backgroundColor = ThemeUtils.getColor(context, R.attr.vctr_message_bubble_inbound) shapeAppearanceModelBuilder .setTopRightCorner(CornerFamily.ROUNDED, cornerRadius) .setBottomRightCorner(CornerFamily.ROUNDED, cornerRadius) .setTopLeftCorner(topCornerFamily, topRadius) .setBottomLeftCorner(bottomCornerFamily, bottomRadius) } else { - val resolvedColor = ContextCompat.getColor(context, R.color.palette_element_green) - val alpha = if (ThemeUtils.isLightTheme(context)) 0x0E else 0x26 - backgroundColor = ColorUtils.setAlphaComponent(resolvedColor, alpha) + backgroundColor = ThemeUtils.getColor(context, R.attr.vctr_message_bubble_outbound) + //val alpha = if (ThemeUtils.isLightTheme(context)) 0x0E else 0x26 + //backgroundColor = ColorUtils.setAlphaComponent(resolvedColor, alpha) shapeAppearanceModelBuilder .setTopLeftCorner(CornerFamily.ROUNDED, cornerRadius) .setBottomLeftCorner(CornerFamily.ROUNDED, cornerRadius) From 8f0e1039aa8516772e0c60acb666fbe74d5a7e4e Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Mon, 24 Jan 2022 19:31:51 +0100 Subject: [PATCH 21/36] Bubbles: make round style algorithm more accurate --- .../style/TimelineMessageLayoutFactory.kt | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index e12e27472c..952d59e372 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -81,26 +81,25 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess buildModernLayout(showInformation) } TimelineLayoutSettings.BUBBLE -> { - val type = event.root.getClearType() - if (type in EVENT_TYPES_WITH_BUBBLE_LAYOUT) { - val messageContent = event.getLastMessageContent() - if (messageContent?.msgType in MSG_TYPES_WITHOUT_BUBBLE_LAYOUT) { - buildModernLayout(showInformation) - } else { - val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator - val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId || - prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate() + val shouldBuildBubbleLayout = event.shouldBuildBubbleLayout() + if (shouldBuildBubbleLayout) { + val isFirstFromThisSender = nextDisplayableEvent == null || !nextDisplayableEvent.shouldBuildBubbleLayout() || + nextDisplayableEvent.root.senderId != event.root.senderId || addDaySeparator - TimelineMessageLayout.Bubble( - showAvatar = showInformation && !isSentByMe, - showDisplayName = showInformation && !isSentByMe, - isIncoming = !isSentByMe, - isFirstFromThisSender = isFirstFromThisSender, - isLastFromThisSender = isLastFromThisSender, - isPseudoBubble = messageContent?.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT, - timestampAsOverlay = messageContent?.msgType in MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY - ) - } + val isLastFromThisSender = prevDisplayableEvent == null || !prevDisplayableEvent.shouldBuildBubbleLayout() || + prevDisplayableEvent.root.senderId != event.root.senderId || + prevDisplayableEvent.root.localDateTime().toLocalDate() != date.toLocalDate() + + val messageContent = event.getLastMessageContent() + TimelineMessageLayout.Bubble( + showAvatar = showInformation && !isSentByMe, + showDisplayName = showInformation && !isSentByMe, + isIncoming = !isSentByMe, + isFirstFromThisSender = isFirstFromThisSender, + isLastFromThisSender = isLastFromThisSender, + isPseudoBubble = messageContent?.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT, + timestampAsOverlay = messageContent?.msgType in MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY + ) } else { buildModernLayout(showInformation) } @@ -109,6 +108,18 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess return messageLayout } + private fun TimelineEvent.shouldBuildBubbleLayout(): Boolean { + val type = root.getClearType() + if (type in EVENT_TYPES_WITH_BUBBLE_LAYOUT) { + val messageContent = getLastMessageContent() + if (messageContent?.msgType in MSG_TYPES_WITHOUT_BUBBLE_LAYOUT) { + return false + } + return true + } + return false + } + private fun buildModernLayout(showInformation: Boolean): TimelineMessageLayout.Default { return TimelineMessageLayout.Default( showAvatar = showInformation, From a9fe21e583d678041e33900167727c1a0746d990 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Thu, 27 Jan 2022 18:17:23 +0100 Subject: [PATCH 22/36] Timeline html rendering: handle code tags --- .../timeline/factory/MessageItemFactory.kt | 60 ++----------- .../timeline/item/MessageBlockCodeItem.kt | 57 ------------ .../vector/app/features/html/CodeVisitor.kt | 55 ------------ .../app/features/html/EventHtmlRenderer.kt | 2 + .../app/features/html/HtmlCodeHandlers.kt | 60 +++++++++++++ .../vector/app/features/html/HtmlCodeSpan.kt | 86 +++++++++++++++++++ .../app/features/html/ParagraphHandler.kt | 18 ++-- 7 files changed, 161 insertions(+), 177 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt delete mode 100644 vector/src/main/java/im/vector/app/features/html/CodeVisitor.kt create mode 100644 vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt create mode 100644 vector/src/main/java/im/vector/app/features/html/HtmlCodeSpan.kt 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 c004a2f928..2fc996c28d 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 @@ -43,8 +43,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttrib import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem -import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem -import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem @@ -63,7 +61,6 @@ import im.vector.app.features.home.room.detail.timeline.item.VerificationRequest import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.linkify -import im.vector.app.features.html.CodeVisitor import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.SpanUtils @@ -71,7 +68,6 @@ import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer import me.gujun.android.span.span -import org.commonmark.node.Document import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session @@ -454,46 +450,22 @@ class MessageItemFactory @Inject constructor( highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { - val isFormatted = messageContent.matrixFormattedBody.isNullOrBlank().not() - return if (isFormatted) { - // First detect if the message contains some code block(s) or inline code - val localFormattedBody = htmlRenderer.get().parse(messageContent.body) as Document - val codeVisitor = CodeVisitor() - codeVisitor.visit(localFormattedBody) - when (codeVisitor.codeKind) { - CodeVisitor.Kind.BLOCK -> { - val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody) - if (codeFormattedBlock == null) { - buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) - } else { - buildCodeBlockItem(codeFormattedBlock, informationData, highlight, callback, attributes) - } - } - CodeVisitor.Kind.INLINE -> { - val codeFormatted = htmlRenderer.get().render(localFormattedBody) - if (codeFormatted == null) { - buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) - } else { - buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes) - } - } - CodeVisitor.Kind.NONE -> { - buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes) - } - } + val matrixFormattedBody = messageContent.matrixFormattedBody + return if (matrixFormattedBody != null) { + buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes) } else { buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) } } - private fun buildFormattedTextItem(messageContent: MessageTextContent, + private fun buildFormattedTextItem(matrixFormattedBody: String, informationData: MessageInformationData, highlight: Boolean, callback: TimelineEventController.Callback?, attributes: AbsMessageItem.Attributes): MessageTextItem? { - val compressed = htmlCompressor.compress(messageContent.formattedBody!!) - val formattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) - return buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes) + val compressed = htmlCompressor.compress(matrixFormattedBody) + val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned + return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes) } private fun buildMessageTextItem(body: CharSequence, @@ -526,24 +498,6 @@ class MessageItemFactory @Inject constructor( .movementMethod(createLinkMovementMethod(callback)) } - private fun buildCodeBlockItem(formattedBody: CharSequence, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes): MessageBlockCodeItem? { - return MessageBlockCodeItem_() - .apply { - if (informationData.hasBeenEdited) { - val spannable = annotateWithEdited("", callback, informationData) - editedSpan(spannable.toEpoxyCharSequence()) - } - } - .leftGuideline(avatarSizeProvider.leftGuideline) - .attributes(attributes) - .highlighted(highlight) - .message(formattedBody.toEpoxyCharSequence()) - } - private fun annotateWithEdited(linkifiedBody: CharSequence, callback: TimelineEventController.Callback?, informationData: MessageInformationData): Spannable { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt deleted file mode 100644 index 9e162a8f1e..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageBlockCodeItem.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2019 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.item - -import android.widget.TextView -import com.airbnb.epoxy.EpoxyAttribute -import com.airbnb.epoxy.EpoxyModelClass -import im.vector.app.R -import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence -import im.vector.app.core.epoxy.onClick -import im.vector.app.core.extensions.setTextOrHide -import me.saket.bettermovementmethod.BetterLinkMovementMethod - -@EpoxyModelClass(layout = R.layout.item_timeline_event_base) -abstract class MessageBlockCodeItem : AbsMessageItem<MessageBlockCodeItem.Holder>() { - - @EpoxyAttribute - var message: EpoxyCharSequence? = null - - @EpoxyAttribute - var editedSpan: EpoxyCharSequence? = null - - override fun bind(holder: Holder) { - super.bind(holder) - holder.messageView.text = message?.charSequence - renderSendState(holder.messageView, holder.messageView) - holder.messageView.onClick(attributes.itemClickListener) - holder.messageView.setOnLongClickListener(attributes.itemLongClickListener) - holder.editedView.movementMethod = BetterLinkMovementMethod.getInstance() - holder.editedView.setTextOrHide(editedSpan?.charSequence) - } - - override fun getViewStubId() = STUB_ID - - class Holder : AbsMessageItem.Holder(STUB_ID) { - val messageView by bind<TextView>(R.id.codeBlockTextView) - val editedView by bind<TextView>(R.id.codeBlockEditedView) - } - - companion object { - private const val STUB_ID = R.id.messageContentCodeBlockStub - } -} diff --git a/vector/src/main/java/im/vector/app/features/html/CodeVisitor.kt b/vector/src/main/java/im/vector/app/features/html/CodeVisitor.kt deleted file mode 100644 index f1612c3717..0000000000 --- a/vector/src/main/java/im/vector/app/features/html/CodeVisitor.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2019 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.html - -import org.commonmark.node.AbstractVisitor -import org.commonmark.node.Code -import org.commonmark.node.FencedCodeBlock -import org.commonmark.node.IndentedCodeBlock - -/** - * This class is in charge of visiting nodes and tells if we have some code nodes (inline or block). - */ -class CodeVisitor : AbstractVisitor() { - - var codeKind: Kind = Kind.NONE - private set - - override fun visit(fencedCodeBlock: FencedCodeBlock?) { - if (codeKind == Kind.NONE) { - codeKind = Kind.BLOCK - } - } - - override fun visit(indentedCodeBlock: IndentedCodeBlock?) { - if (codeKind == Kind.NONE) { - codeKind = Kind.BLOCK - } - } - - override fun visit(code: Code?) { - if (codeKind == Kind.NONE) { - codeKind = Kind.INLINE - } - } - - enum class Kind { - NONE, - INLINE, - BLOCK - } -} diff --git a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt index 5fc1306b42..7d78be3584 100644 --- a/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/html/EventHtmlRenderer.kt @@ -121,6 +121,8 @@ class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: C .addHandler(FontTagHandler()) .addHandler(ParagraphHandler(DimensionConverter(resources))) .addHandler(MxReplyTagHandler()) + .addHandler(CodePreTagHandler()) + .addHandler(CodeTagHandler()) .addHandler(SpanHandler(colorProvider)) } } diff --git a/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt b/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt new file mode 100644 index 0000000000..1010625370 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/html/HtmlCodeHandlers.kt @@ -0,0 +1,60 @@ +/* + * 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.html + +import io.noties.markwon.MarkwonVisitor +import io.noties.markwon.SpannableBuilder +import io.noties.markwon.html.HtmlTag +import io.noties.markwon.html.MarkwonHtmlRenderer +import io.noties.markwon.html.TagHandler + +class CodeTagHandler : TagHandler() { + + override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { + SpannableBuilder.setSpans( + visitor.builder(), + HtmlCodeSpan(visitor.configuration().theme(), false), + tag.start(), + tag.end() + ) + } + + override fun supportedTags(): List<String> { + return listOf("code") + } +} + +/** + * Pre tag are already handled by HtmlPlugin to keep the formatting. + * We are only using it to check for <pre><code>*</code></pre> tags. + */ +class CodePreTagHandler : TagHandler() { + override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { + val htmlCodeSpan = visitor.builder() + .getSpans(tag.start(), tag.end()) + .firstOrNull { + it.what is HtmlCodeSpan + } + if (htmlCodeSpan != null) { + (htmlCodeSpan.what as HtmlCodeSpan).isBlock = true + } + } + + override fun supportedTags(): List<String> { + return listOf("pre") + } +} diff --git a/vector/src/main/java/im/vector/app/features/html/HtmlCodeSpan.kt b/vector/src/main/java/im/vector/app/features/html/HtmlCodeSpan.kt new file mode 100644 index 0000000000..7f01321aab --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/html/HtmlCodeSpan.kt @@ -0,0 +1,86 @@ +/* + * 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.html + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Rect +import android.text.Layout +import android.text.TextPaint +import android.text.style.LeadingMarginSpan +import android.text.style.MetricAffectingSpan +import io.noties.markwon.core.MarkwonTheme + +class HtmlCodeSpan(private val theme: MarkwonTheme, var isBlock: Boolean) : MetricAffectingSpan(), LeadingMarginSpan { + + private val rect = Rect() + private val paint = Paint() + + override fun updateDrawState(p: TextPaint) { + applyTextStyle(p) + if (!isBlock) { + p.bgColor = theme.getCodeBackgroundColor(p) + } + } + + override fun updateMeasureState(p: TextPaint) { + applyTextStyle(p) + } + + private fun applyTextStyle(p: TextPaint) { + if (isBlock) { + theme.applyCodeBlockTextStyle(p) + } else { + theme.applyCodeTextStyle(p) + } + } + + override fun getLeadingMargin(first: Boolean): Int { + return theme.codeBlockMargin + } + + override fun drawLeadingMargin( + c: Canvas, + p: Paint?, + x: Int, + dir: Int, + top: Int, + baseline: Int, + bottom: Int, + text: CharSequence?, + start: Int, + end: Int, + first: Boolean, + layout: Layout? + ) { + if (!isBlock) return + + paint.style = Paint.Style.FILL + paint.color = theme.getCodeBlockBackgroundColor(p!!) + val left: Int + val right: Int + if (dir > 0) { + left = x + right = c.width + } else { + left = x - c.width + right = x + } + rect[left, top, right] = bottom + c.drawRect(rect, paint) + } +} diff --git a/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt b/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt index e62134ae7c..3dd1b4f091 100644 --- a/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt +++ b/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt @@ -16,7 +16,6 @@ package im.vector.app.features.html -import android.content.res.Resources import im.vector.app.core.utils.DimensionConverter import io.noties.markwon.MarkwonVisitor import io.noties.markwon.SpannableBuilder @@ -24,7 +23,6 @@ import io.noties.markwon.html.HtmlTag import io.noties.markwon.html.MarkwonHtmlRenderer import io.noties.markwon.html.TagHandler import me.gujun.android.span.style.VerticalPaddingSpan -import org.commonmark.node.BlockQuote class ParagraphHandler(private val dimensionConverter: DimensionConverter) : TagHandler() { @@ -34,15 +32,11 @@ class ParagraphHandler(private val dimensionConverter: DimensionConverter) : Tag if (tag.isBlock) { visitChildren(visitor, renderer, tag.asBlock) } - val configuration = visitor.configuration() - val factory = configuration.spansFactory().get(BlockQuote::class.java) - if (factory != null) { - SpannableBuilder.setSpans( - visitor.builder(), - VerticalPaddingSpan(dimensionConverter.dpToPx(16), 0), - tag.start(), - tag.end() - ) - } + SpannableBuilder.setSpans( + visitor.builder(), + VerticalPaddingSpan(dimensionConverter.dpToPx(4), dimensionConverter.dpToPx(4)), + tag.start(), + tag.end() + ) } } From 4de421d663f409813de6f0ec07d8b65906d9d039 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Thu, 27 Jan 2022 18:17:56 +0100 Subject: [PATCH 23/36] Timeline html rendering: better reply and pill --- .../app/features/html/MxReplyTagHandler.kt | 17 +++-------------- .../vector/app/features/html/PillImageSpan.kt | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/html/MxReplyTagHandler.kt b/vector/src/main/java/im/vector/app/features/html/MxReplyTagHandler.kt index 391c5f9477..118369e3c8 100644 --- a/vector/src/main/java/im/vector/app/features/html/MxReplyTagHandler.kt +++ b/vector/src/main/java/im/vector/app/features/html/MxReplyTagHandler.kt @@ -17,28 +17,17 @@ package im.vector.app.features.html import io.noties.markwon.MarkwonVisitor -import io.noties.markwon.SpannableBuilder import io.noties.markwon.html.HtmlTag import io.noties.markwon.html.MarkwonHtmlRenderer import io.noties.markwon.html.TagHandler -import org.commonmark.node.BlockQuote class MxReplyTagHandler : TagHandler() { override fun supportedTags() = listOf("mx-reply") override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) { - val configuration = visitor.configuration() - val factory = configuration.spansFactory().get(BlockQuote::class.java) - if (factory != null) { - SpannableBuilder.setSpans( - visitor.builder(), - factory.getSpans(configuration, visitor.renderProps()), - tag.start(), - tag.end() - ) - val replyText = visitor.builder().removeFromEnd(tag.end()) - visitor.builder().append("\n\n").append(replyText) - } + visitChildren(visitor, renderer, tag.asBlock) + val replyText = visitor.builder().removeFromEnd(tag.end()) + visitor.builder().append("\n\n").append(replyText) } } diff --git a/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt b/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt index c1040a8cc0..ff2e2a9cdb 100644 --- a/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt +++ b/vector/src/main/java/im/vector/app/features/html/PillImageSpan.kt @@ -65,10 +65,15 @@ class PillImageSpan(private val glideRequests: GlideRequests, fm: Paint.FontMetricsInt?): Int { val rect = pillDrawable.bounds if (fm != null) { - fm.ascent = -rect.bottom - fm.descent = 0 - fm.top = fm.ascent - fm.bottom = 0 + val fmPaint = paint.fontMetricsInt + val fontHeight = fmPaint.bottom - fmPaint.top + val drHeight = rect.bottom - rect.top + val top = drHeight / 2 - fontHeight / 4 + val bottom = drHeight / 2 + fontHeight / 4 + fm.ascent = -bottom + fm.top = -bottom + fm.bottom = top + fm.descent = top } return rect.right } @@ -82,7 +87,9 @@ class PillImageSpan(private val glideRequests: GlideRequests, bottom: Int, paint: Paint) { canvas.save() - val transY = bottom - pillDrawable.bounds.bottom + val fm = paint.fontMetricsInt + val transY: Int = y + (fm.descent + fm.ascent - pillDrawable.bounds.bottom) / 2 + canvas.save() canvas.translate(x, transY.toFloat()) pillDrawable.draw(canvas) canvas.restore() From 881b063d452b31ab74968a055355e569bf4ce818 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Thu, 27 Jan 2022 18:18:29 +0100 Subject: [PATCH 24/36] Bubbles: remove emote from bubble (keep right/left alignment) --- .../detail/timeline/style/TimelineMessageLayoutFactory.kt | 5 ++++- .../home/room/detail/timeline/view/MessageBubbleView.kt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index 952d59e372..e5ba544657 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -48,7 +48,10 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess // Use the bubble layout but without borders private val MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT = setOf( - MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_STICKER_LOCAL + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_STICKER_LOCAL, + MessageType.MSGTYPE_EMOTE ) private val MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY = setOf( MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_VIDEO diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index f1250f7efe..4a21cffed6 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -103,7 +103,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } applyTo(views.bubbleView) } - if (messageLayout.isPseudoBubble) { + if (messageLayout.isPseudoBubble && messageLayout.timestampAsOverlay) { views.viewStubContainer.root.setPadding(0, 0, 0, 0) } else { views.viewStubContainer.root.setPadding(horizontalStubPadding, verticalStubPadding, horizontalStubPadding, verticalStubPadding) From ed9adf83672253ff92470ddcae1fa3bea79557cd Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Thu, 27 Jan 2022 18:18:39 +0100 Subject: [PATCH 25/36] Bubbles: fix paddings --- .../src/main/res/values/styles_timeline.xml | 7 +++++++ .../res/layout/item_timeline_event_base_noinfo.xml | 1 + .../res/layout/item_timeline_event_base_state.xml | 1 + .../item_timeline_event_view_stubs_container.xml | 14 +------------- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/library/ui-styles/src/main/res/values/styles_timeline.xml b/library/ui-styles/src/main/res/values/styles_timeline.xml index 127d9f3dc7..3bd3543de2 100644 --- a/library/ui-styles/src/main/res/values/styles_timeline.xml +++ b/library/ui-styles/src/main/res/values/styles_timeline.xml @@ -6,6 +6,13 @@ <item name="android:layout_height">wrap_content</item> </style> + <style name="TimelineContentStubContainerParams"> + <item name="android:paddingStart">8dp</item> + <item name="android:paddingEnd">8dp</item> + <item name="android:paddingTop">4dp</item> + <item name="android:paddingBottom">4dp</item> + </style> + <style name="TimelineContentMediaPillStyle"> <item name="android:paddingStart">8dp</item> <item name="android:paddingEnd">8dp</item> diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index f400fc6a9a..731642cabd 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -26,6 +26,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" + style="@style/TimelineContentStubContainerParams" android:layout_toEndOf="@id/messageStartGuideline"> <ViewStub diff --git a/vector/src/main/res/layout/item_timeline_event_base_state.xml b/vector/src/main/res/layout/item_timeline_event_base_state.xml index ba546c040f..927abbfd16 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_state.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_state.xml @@ -31,6 +31,7 @@ <FrameLayout android:id="@+id/viewStubContainer" + style="@style/TimelineContentStubContainerParams" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" diff --git a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml index 440a1f4602..1fea380b1f 100644 --- a/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml +++ b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml @@ -4,12 +4,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:addStatesFromChildren="true" - android:paddingStart="8dp" - android:paddingLeft="8dp" - android:paddingTop="4dp" - android:paddingEnd="8dp" - android:paddingRight="8dp" - android:paddingBottom="4dp"> + style="@style/TimelineContentStubContainerParams"> <ViewStub android:id="@+id/messageContentTextStub" @@ -18,13 +13,6 @@ android:layout="@layout/item_timeline_event_text_message_stub" tools:visibility="visible" /> - <ViewStub - android:id="@+id/messageContentCodeBlockStub" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout="@layout/item_timeline_event_code_block_stub" - tools:visibility="visible" /> - <ViewStub android:id="@+id/messageContentMediaStub" style="@style/TimelineContentStubBaseParams" From b79a5fd4f4a944be2cea7581c3b75a5d5f58edbd Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Fri, 28 Jan 2022 15:09:43 +0100 Subject: [PATCH 26/36] Bubble: move overlay view to MessageBubbleView (and fix corner radius) --- .../timeline/item/MessageImageVideoItem.kt | 2 - .../style/TimelineMessageLayoutExt.kt | 48 ++++++++++++++++ .../detail/timeline/view/MessageBubbleView.kt | 55 ++++++++----------- .../res/drawable/overlay_bubble_media.xml | 1 - ...item_timeline_event_media_message_stub.xml | 11 ---- .../main/res/layout/view_message_bubble.xml | 12 ++++ 6 files changed, 82 insertions(+), 47 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutExt.kt 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 274841973e..2ac84e6ff6 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 @@ -89,7 +89,6 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold holder.mediaContentView.onClick(attributes.itemClickListener) holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener) holder.playContentView.visibility = if (playable) View.VISIBLE else View.GONE - holder.overlayView.isVisible = messageLayout is TimelineMessageLayout.Bubble && messageLayout.timestampAsOverlay } override fun unbind(holder: Holder) { @@ -108,7 +107,6 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold val imageView by bind<ImageView>(R.id.messageThumbnailView) val playContentView by bind<ImageView>(R.id.messageMediaPlayView) val mediaContentView by bind<ViewGroup>(R.id.messageContentMedia) - val overlayView by bind<View>(R.id.messageMediaOverlayView) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutExt.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutExt.kt new file mode 100644 index 0000000000..503c9c7a6d --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutExt.kt @@ -0,0 +1,48 @@ +/* + * 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.style + +import com.google.android.material.shape.CornerFamily +import com.google.android.material.shape.ShapeAppearanceModel + +fun TimelineMessageLayout.Bubble.shapeAppearanceModel(cornerRadius: Float): ShapeAppearanceModel { + val (topCornerFamily, topRadius) = if (isFirstFromThisSender) { + Pair(CornerFamily.ROUNDED, cornerRadius) + } else { + Pair(CornerFamily.CUT, 0f) + } + val (bottomCornerFamily, bottomRadius) = if (isLastFromThisSender) { + Pair(CornerFamily.ROUNDED, cornerRadius) + } else { + Pair(CornerFamily.CUT, 0f) + } + val shapeAppearanceModelBuilder = ShapeAppearanceModel().toBuilder() + if (isIncoming) { + shapeAppearanceModelBuilder + .setTopRightCorner(CornerFamily.ROUNDED, cornerRadius) + .setBottomRightCorner(CornerFamily.ROUNDED, cornerRadius) + .setTopLeftCorner(topCornerFamily, topRadius) + .setBottomLeftCorner(bottomCornerFamily, bottomRadius) + } else { + shapeAppearanceModelBuilder + .setTopLeftCorner(CornerFamily.ROUNDED, cornerRadius) + .setBottomLeftCorner(CornerFamily.ROUNDED, cornerRadius) + .setTopRightCorner(topCornerFamily, topRadius) + .setBottomRightCorner(bottomCornerFamily, bottomRadius) + } + return shapeAppearanceModelBuilder.build() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 4a21cffed6..49771d1712 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.res.ColorStateList import android.graphics.Color import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable import android.util.AttributeSet import android.view.View import android.view.ViewOutlineProvider @@ -27,15 +28,14 @@ import android.widget.RelativeLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes -import androidx.core.graphics.ColorUtils +import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams -import com.google.android.material.shape.CornerFamily import com.google.android.material.shape.MaterialShapeDrawable -import com.google.android.material.shape.ShapeAppearanceModel import im.vector.app.R import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.ViewMessageBubbleBinding 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 import timber.log.Timber @@ -103,6 +103,12 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } applyTo(views.bubbleView) } + if (messageLayout.timestampAsOverlay) { + views.messageOverlayView.isVisible = true + (views.messageOverlayView.background as? GradientDrawable)?.cornerRadii = messageLayout.cornerRadii(cornerRadius) + } else { + views.messageOverlayView.isVisible = false + } if (messageLayout.isPseudoBubble && messageLayout.timestampAsOverlay) { views.viewStubContainer.root.setPadding(0, 0, 0, 0) } else { @@ -125,41 +131,24 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } } + private fun TimelineMessageLayout.Bubble.cornerRadii(cornerRadius: Float): FloatArray { + val topRadius = if (isFirstFromThisSender) cornerRadius else 0f + val bottomRadius = if (isLastFromThisSender) cornerRadius else 0f + return if (isIncoming) { + floatArrayOf(topRadius, topRadius, cornerRadius, cornerRadius, cornerRadius, cornerRadius, bottomRadius, bottomRadius) + } else { + floatArrayOf(cornerRadius, cornerRadius, topRadius, topRadius, bottomRadius, bottomRadius, cornerRadius, cornerRadius) + } + } + private fun createBackgroundDrawable(messageLayout: TimelineMessageLayout.Bubble): Drawable { - val (topCornerFamily, topRadius) = if (messageLayout.isFirstFromThisSender) { - Pair(CornerFamily.ROUNDED, cornerRadius) - } else { - Pair(CornerFamily.CUT, 0f) - } - val (bottomCornerFamily, bottomRadius) = if (messageLayout.isLastFromThisSender) { - Pair(CornerFamily.ROUNDED, cornerRadius) - } else { - Pair(CornerFamily.CUT, 0f) - } - val shapeAppearanceModelBuilder = ShapeAppearanceModel().toBuilder() - val backgroundColor: Int - if (isIncoming) { - backgroundColor = ThemeUtils.getColor(context, R.attr.vctr_message_bubble_inbound) - shapeAppearanceModelBuilder - .setTopRightCorner(CornerFamily.ROUNDED, cornerRadius) - .setBottomRightCorner(CornerFamily.ROUNDED, cornerRadius) - .setTopLeftCorner(topCornerFamily, topRadius) - .setBottomLeftCorner(bottomCornerFamily, bottomRadius) - } else { - backgroundColor = ThemeUtils.getColor(context, R.attr.vctr_message_bubble_outbound) - //val alpha = if (ThemeUtils.isLightTheme(context)) 0x0E else 0x26 - //backgroundColor = ColorUtils.setAlphaComponent(resolvedColor, alpha) - shapeAppearanceModelBuilder - .setTopLeftCorner(CornerFamily.ROUNDED, cornerRadius) - .setBottomLeftCorner(CornerFamily.ROUNDED, cornerRadius) - .setTopRightCorner(topCornerFamily, topRadius) - .setBottomRightCorner(bottomCornerFamily, bottomRadius) - } - val shapeAppearanceModel = shapeAppearanceModelBuilder.build() + val shapeAppearanceModel = messageLayout.shapeAppearanceModel(cornerRadius) return MaterialShapeDrawable(shapeAppearanceModel).apply { fillColor = if (messageLayout.isPseudoBubble) { ColorStateList.valueOf(Color.TRANSPARENT) } else { + val backgroundColorAttr = if (isIncoming) R.attr.vctr_message_bubble_inbound else R.attr.vctr_message_bubble_outbound + val backgroundColor = ThemeUtils.getColor(context, backgroundColorAttr) ColorStateList.valueOf(backgroundColor) } } diff --git a/vector/src/main/res/drawable/overlay_bubble_media.xml b/vector/src/main/res/drawable/overlay_bubble_media.xml index 8bc9c58064..ce34a39037 100644 --- a/vector/src/main/res/drawable/overlay_bubble_media.xml +++ b/vector/src/main/res/drawable/overlay_bubble_media.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> - <corners android:radius="@dimen/chat_bubble_corner_radius"/> <gradient android:type="linear" android:angle="270" 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 bdfa0c5164..7b3c5199b7 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 @@ -18,17 +18,6 @@ tools:layout_height="300dp" tools:src="@tools:sample/backgrounds/scenic" /> - <View - android:id="@+id/messageMediaOverlayView" - android:layout_width="0dp" - android:layout_height="0dp" - android:background="@drawable/overlay_bubble_media" - android:elevation="2dp" - app:layout_constraintStart_toStartOf="@id/messageThumbnailView" - app:layout_constraintEnd_toEndOf="@id/messageThumbnailView" - app:layout_constraintTop_toTopOf="@id/messageThumbnailView" - app:layout_constraintBottom_toBottomOf="@id/messageThumbnailView"/> - <ImageView android:id="@+id/messageMediaPlayView" android:layout_width="40dp" diff --git a/vector/src/main/res/layout/view_message_bubble.xml b/vector/src/main/res/layout/view_message_bubble.xml index d6263b96d7..1ffb358d4e 100644 --- a/vector/src/main/res/layout/view_message_bubble.xml +++ b/vector/src/main/res/layout/view_message_bubble.xml @@ -105,6 +105,18 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintWidth_max="@dimen/chat_bubble_fixed_size" /> + <View + android:id="@+id/messageOverlayView" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@drawable/overlay_bubble_media" + android:visibility="gone" + tools:visibility="visible" + app:layout_constraintStart_toStartOf="@id/viewStubContainer" + app:layout_constraintEnd_toEndOf="@id/viewStubContainer" + app:layout_constraintTop_toTopOf="@id/viewStubContainer" + app:layout_constraintBottom_toBottomOf="@id/viewStubContainer"/> + <TextView android:id="@+id/messageTimeView" android:layout_width="wrap_content" From 35674ad401189620887bdc0b3f18ae943356a07b Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Fri, 28 Jan 2022 15:10:13 +0100 Subject: [PATCH 27/36] Bubbles: handle location sharing (need updates) --- .../timeline/item/MessageLocationItem.kt | 11 +++++++++- .../style/TimelineMessageLayoutFactory.kt | 22 ++++++++++++++----- .../item_timeline_event_location_stub.xml | 5 +++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt index 51d95570c0..6d91ef2d14 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt @@ -20,9 +20,14 @@ import android.widget.FrameLayout import androidx.constraintlayout.widget.ConstraintLayout import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.google.android.material.card.MaterialCardView import im.vector.app.R import im.vector.app.core.epoxy.onClick import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +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.location.LocationData import im.vector.app.features.location.MapTilerMapView @@ -48,9 +53,12 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>( override fun bind(holder: Holder) { super.bind(holder) renderSendState(holder.mapViewContainer, null) - val location = locationData ?: return val locationOwnerId = userId ?: return + val messageLayout = attributes.informationData.messageLayout + if (messageLayout is TimelineMessageLayout.Bubble) { + holder.mapCardView.shapeAppearanceModel = messageLayout.shapeAppearanceModel(12f) + } holder.clickableMapArea.onClick { callback?.onMapClicked() @@ -71,6 +79,7 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>( override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { + val mapCardView by bind<MaterialCardView>(R.id.mapCardView) val mapViewContainer by bind<ConstraintLayout>(R.id.mapViewContainer) val mapView by bind<MapTilerMapView>(R.id.mapView) val clickableMapArea by bind<FrameLayout>(R.id.clickableMapArea) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index e5ba544657..2f6ec15fde 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -21,6 +21,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFact import im.vector.app.features.settings.VectorPreferences 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.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -100,8 +101,8 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess isIncoming = !isSentByMe, isFirstFromThisSender = isFirstFromThisSender, isLastFromThisSender = isLastFromThisSender, - isPseudoBubble = messageContent?.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT, - timestampAsOverlay = messageContent?.msgType in MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY + isPseudoBubble = messageContent.isPseudoBubble(), + timestampAsOverlay = messageContent.timestampAsOverlay() ) } else { buildModernLayout(showInformation) @@ -111,14 +112,23 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess return messageLayout } + private fun MessageContent?.isPseudoBubble(): Boolean{ + if(this == null) return false + if(msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline() + return this.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT + } + + private fun MessageContent?.timestampAsOverlay(): Boolean{ + if(this == null) return false + if(msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline() + return this.msgType in MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY + } + private fun TimelineEvent.shouldBuildBubbleLayout(): Boolean { val type = root.getClearType() if (type in EVENT_TYPES_WITH_BUBBLE_LAYOUT) { val messageContent = getLastMessageContent() - if (messageContent?.msgType in MSG_TYPES_WITHOUT_BUBBLE_LAYOUT) { - return false - } - return true + return messageContent?.msgType !in MSG_TYPES_WITHOUT_BUBBLE_LAYOUT } return false } diff --git a/vector/src/main/res/layout/item_timeline_event_location_stub.xml b/vector/src/main/res/layout/item_timeline_event_location_stub.xml index b2f68b2fc3..134e9e005d 100644 --- a/vector/src/main/res/layout/item_timeline_event_location_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_location_stub.xml @@ -1,13 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/mapCardView" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" - android:layout_height="wrap_content" - app:cardCornerRadius="8dp"> + android:layout_height="wrap_content"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/mapViewContainer" + android:minWidth="@dimen/chat_bubble_fixed_size" android:layout_width="match_parent" android:layout_height="wrap_content"> From c425701c209441290feae6d38f21b88ef3b42938 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Fri, 28 Jan 2022 17:55:32 +0100 Subject: [PATCH 28/36] Bubbles: handle ripple effect --- .../detail/timeline/view/MessageBubbleView.kt | 30 +++++++++++++------ ...em_timeline_event_bubble_incoming_base.xml | 1 - ...em_timeline_event_bubble_outgoing_base.xml | 1 - 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 49771d1712..111f5c880d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -21,6 +21,7 @@ import android.content.res.ColorStateList import android.graphics.Color import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.RippleDrawable import android.util.AttributeSet import android.view.View import android.view.ViewOutlineProvider @@ -28,6 +29,7 @@ import android.widget.RelativeLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes +import androidx.core.graphics.drawable.DrawableCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import com.google.android.material.shape.MaterialShapeDrawable @@ -50,6 +52,8 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri private val verticalStubPadding = DimensionConverter(resources).dpToPx(4) private lateinit var views: ViewMessageBubbleBinding + private lateinit var bubbleDrawable: MaterialShapeDrawable + private lateinit var rippleMaskDrawable: MaterialShapeDrawable init { inflate(context, R.layout.view_message_bubble, this) @@ -72,11 +76,21 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } else { View.LAYOUT_DIRECTION_LTR } - views.informationBottom.layoutDirection = oppositeLayoutDirection views.bubbleWrapper.layoutDirection = oppositeLayoutDirection views.bubbleView.layoutDirection = currentLayoutDirection } + bubbleDrawable = MaterialShapeDrawable() + rippleMaskDrawable = MaterialShapeDrawable() + DrawableCompat.setTint(rippleMaskDrawable, Color.WHITE) + views.bubbleView.apply { + outlineProvider = ViewOutlineProvider.BACKGROUND + clipToOutline = true + background = RippleDrawable( + ContextCompat.getColorStateList(context, R.color.mtrl_btn_ripple_color) ?: ColorStateList.valueOf(Color.TRANSPARENT), + bubbleDrawable, + rippleMaskDrawable) + } } override fun render(messageLayout: TimelineMessageLayout) { @@ -84,11 +98,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri Timber.v("Can't render messageLayout $messageLayout") return } - views.bubbleView.apply { - background = createBackgroundDrawable(messageLayout) - outlineProvider = ViewOutlineProvider.BACKGROUND - clipToOutline = true - } + updateDrawables(messageLayout) ConstraintSet().apply { clone(views.bubbleView) clear(R.id.viewStubContainer, ConstraintSet.END) @@ -141,10 +151,11 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } } - private fun createBackgroundDrawable(messageLayout: TimelineMessageLayout.Bubble): Drawable { + private fun updateDrawables(messageLayout: TimelineMessageLayout.Bubble) { val shapeAppearanceModel = messageLayout.shapeAppearanceModel(cornerRadius) - return MaterialShapeDrawable(shapeAppearanceModel).apply { - fillColor = if (messageLayout.isPseudoBubble) { + bubbleDrawable.apply { + this.shapeAppearanceModel = shapeAppearanceModel + this.fillColor = if (messageLayout.isPseudoBubble) { ColorStateList.valueOf(Color.TRANSPARENT) } else { val backgroundColorAttr = if (isIncoming) R.attr.vctr_message_bubble_inbound else R.attr.vctr_message_bubble_outbound @@ -152,5 +163,6 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri ColorStateList.valueOf(backgroundColor) } } + rippleMaskDrawable.shapeAppearanceModel = shapeAppearanceModel } } diff --git a/vector/src/main/res/layout/item_timeline_event_bubble_incoming_base.xml b/vector/src/main/res/layout/item_timeline_event_bubble_incoming_base.xml index ac8e3aad48..5052efb46e 100644 --- a/vector/src/main/res/layout/item_timeline_event_bubble_incoming_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_bubble_incoming_base.xml @@ -4,5 +4,4 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:addStatesFromChildren="true" - android:background="?attr/selectableItemBackground" app:incoming_style="true" /> diff --git a/vector/src/main/res/layout/item_timeline_event_bubble_outgoing_base.xml b/vector/src/main/res/layout/item_timeline_event_bubble_outgoing_base.xml index 42bf1b5f7a..0a58a53b88 100644 --- a/vector/src/main/res/layout/item_timeline_event_bubble_outgoing_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_bubble_outgoing_base.xml @@ -4,5 +4,4 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:addStatesFromChildren="true" - android:background="?attr/selectableItemBackground" app:incoming_style="false" /> From fd99d6d7d820c140831f67792085499a6ec0c4de Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Fri, 28 Jan 2022 19:53:07 +0100 Subject: [PATCH 29/36] Bubbles: start fixing RTL --- .../detail/timeline/view/MessageBubbleView.kt | 21 ++++++++++--------- .../reactions/widget/ReactionButton.kt | 1 + 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 111f5c880d..a843a408c9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -19,9 +19,9 @@ package im.vector.app.features.home.room.detail.timeline.view import android.content.Context import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.graphics.drawable.RippleDrawable +import android.text.TextUtils import android.util.AttributeSet import android.view.View import android.view.ViewOutlineProvider @@ -34,6 +34,7 @@ import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import com.google.android.material.shape.MaterialShapeDrawable import im.vector.app.R +import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.ViewMessageBubbleBinding import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout @@ -65,21 +66,21 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri override fun onFinishInflate() { super.onFinishInflate() views = ViewMessageBubbleBinding.bind(this) - val currentLayoutDirection = layoutDirection - if (isIncoming) { - views.informationBottom.layoutDirection = currentLayoutDirection - views.bubbleWrapper.layoutDirection = currentLayoutDirection - views.bubbleView.layoutDirection = currentLayoutDirection + val currentLocale = LocaleProvider(resources).current() + val currentLayoutDirection = TextUtils.getLayoutDirectionFromLocale(currentLocale) + val layoutDirectionToSet = if (isIncoming) { + currentLayoutDirection } else { - val oppositeLayoutDirection = if (currentLayoutDirection == View.LAYOUT_DIRECTION_LTR) { + if (currentLayoutDirection == View.LAYOUT_DIRECTION_LTR) { View.LAYOUT_DIRECTION_RTL } else { View.LAYOUT_DIRECTION_LTR } - views.informationBottom.layoutDirection = oppositeLayoutDirection - views.bubbleWrapper.layoutDirection = oppositeLayoutDirection - views.bubbleView.layoutDirection = currentLayoutDirection } + views.informationBottom.layoutDirection = layoutDirectionToSet + views.bubbleWrapper.layoutDirection = layoutDirectionToSet + views.bubbleView.layoutDirection = currentLayoutDirection + bubbleDrawable = MaterialShapeDrawable() rippleMaskDrawable = MaterialShapeDrawable() DrawableCompat.setTint(rippleMaskDrawable, Color.WHITE) diff --git a/vector/src/main/java/im/vector/app/features/reactions/widget/ReactionButton.kt b/vector/src/main/java/im/vector/app/features/reactions/widget/ReactionButton.kt index 67095b974a..cc41141607 100644 --- a/vector/src/main/java/im/vector/app/features/reactions/widget/ReactionButton.kt +++ b/vector/src/main/java/im/vector/app/features/reactions/widget/ReactionButton.kt @@ -70,6 +70,7 @@ class ReactionButton @JvmOverloads constructor(context: Context, orientation = HORIZONTAL minimumHeight = DimensionConverter(context.resources).dpToPx(30) gravity = Gravity.CENTER + layoutDirection = View.LAYOUT_DIRECTION_LOCALE views = ReactionButtonBinding.bind(this) views.reactionCount.text = TextUtils.formatCountToShortDecimal(reactionCount) context.withStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr) { From 820bc644b6066de5ac89aa561d492b0ff3da9988 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Mon, 31 Jan 2022 19:18:42 +0100 Subject: [PATCH 30/36] Bubble: introduce CornersRadius --- .../timeline/item/MessageImageVideoItem.kt | 11 +--- .../timeline/item/MessageLocationItem.kt | 4 -- .../detail/timeline/style/CornersRadius.kt | 38 +++++++++++++ .../timeline/style/TimelineMessageLayout.kt | 14 ++++- .../style/TimelineMessageLayoutExt.kt | 48 ---------------- .../style/TimelineMessageLayoutFactory.kt | 55 ++++++++++++++++--- .../detail/timeline/view/MessageBubbleView.kt | 17 ++---- 7 files changed, 103 insertions(+), 84 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/CornersRadius.kt delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutExt.kt 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 2ac84e6ff6..6f801d9eb7 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 @@ -33,7 +33,7 @@ import im.vector.app.core.glide.GlideApp 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.view.TimelineMessageLayoutRenderer +import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners import im.vector.app.features.media.ImageContentRenderer @EpoxyModelClass(layout = R.layout.item_timeline_event_base) @@ -62,14 +62,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold val messageLayout = baseAttributes.informationData.messageLayout val dimensionConverter = DimensionConverter(holder.view.resources) val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) { - val cornerRadius = holder.view.resources.getDimensionPixelSize(R.dimen.chat_bubble_corner_radius).toFloat() - val topRadius = if (messageLayout.isFirstFromThisSender) cornerRadius else 0f - val bottomRadius = if (messageLayout.isLastFromThisSender) cornerRadius else 0f - if (messageLayout.isIncoming) { - GranularRoundedCorners(topRadius, cornerRadius, cornerRadius, bottomRadius) - } else { - GranularRoundedCorners(cornerRadius, topRadius, bottomRadius, cornerRadius) - } + messageLayout.cornersRadius.granularRoundedCorners() } else { RoundedCorners(dimensionConverter.dpToPx(8)) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt index 6d91ef2d14..6ed4b1bce4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt @@ -55,10 +55,6 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>( renderSendState(holder.mapViewContainer, null) val location = locationData ?: return val locationOwnerId = userId ?: return - val messageLayout = attributes.informationData.messageLayout - if (messageLayout is TimelineMessageLayout.Bubble) { - holder.mapCardView.shapeAppearanceModel = messageLayout.shapeAppearanceModel(12f) - } holder.clickableMapArea.onClick { callback?.onMapClicked() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/CornersRadius.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/CornersRadius.kt new file mode 100644 index 0000000000..c10bebdcf4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/CornersRadius.kt @@ -0,0 +1,38 @@ +/* + * 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.style + +import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners +import com.google.android.material.shape.CornerFamily +import com.google.android.material.shape.ShapeAppearanceModel + +fun TimelineMessageLayout.Bubble.CornersRadius.granularRoundedCorners(): GranularRoundedCorners { + return GranularRoundedCorners(topStartRadius, topEndRadius, bottomEndRadius, bottomStartRadius) +} + +fun TimelineMessageLayout.Bubble.CornersRadius.shapeAppearanceModel(): ShapeAppearanceModel { + return ShapeAppearanceModel().toBuilder() + .setTopRightCorner(topEndRadius.cornerFamily(), topEndRadius) + .setBottomRightCorner(bottomEndRadius.cornerFamily(), bottomEndRadius) + .setTopLeftCorner(topStartRadius.cornerFamily(), topStartRadius) + .setBottomLeftCorner(bottomStartRadius.cornerFamily(), bottomStartRadius) + .build() +} + +private fun Float.cornerFamily(): Int { + return if (this == 0F) CornerFamily.CUT else CornerFamily.ROUNDED +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt index bdcbc52037..c87680de0a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayout.kt @@ -39,14 +39,22 @@ sealed interface TimelineMessageLayout : Parcelable { override val showDisplayName: Boolean, override val showTimestamp: Boolean = true, val isIncoming: Boolean, - val isFirstFromThisSender: Boolean, - val isLastFromThisSender: Boolean, val isPseudoBubble: Boolean, + val cornersRadius: CornersRadius, val timestampAsOverlay: Boolean, override val layoutRes: Int = if (isIncoming) { R.layout.item_timeline_event_bubble_incoming_base } else { R.layout.item_timeline_event_bubble_outgoing_base } - ) : TimelineMessageLayout + ) : TimelineMessageLayout { + + @Parcelize + data class CornersRadius( + val topStartRadius: Float, + val topEndRadius: Float, + val bottomStartRadius: Float, + val bottomEndRadius: Float + ) : Parcelable + } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutExt.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutExt.kt deleted file mode 100644 index 503c9c7a6d..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutExt.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.style - -import com.google.android.material.shape.CornerFamily -import com.google.android.material.shape.ShapeAppearanceModel - -fun TimelineMessageLayout.Bubble.shapeAppearanceModel(cornerRadius: Float): ShapeAppearanceModel { - val (topCornerFamily, topRadius) = if (isFirstFromThisSender) { - Pair(CornerFamily.ROUNDED, cornerRadius) - } else { - Pair(CornerFamily.CUT, 0f) - } - val (bottomCornerFamily, bottomRadius) = if (isLastFromThisSender) { - Pair(CornerFamily.ROUNDED, cornerRadius) - } else { - Pair(CornerFamily.CUT, 0f) - } - val shapeAppearanceModelBuilder = ShapeAppearanceModel().toBuilder() - if (isIncoming) { - shapeAppearanceModelBuilder - .setTopRightCorner(CornerFamily.ROUNDED, cornerRadius) - .setBottomRightCorner(CornerFamily.ROUNDED, cornerRadius) - .setTopLeftCorner(topCornerFamily, topRadius) - .setBottomLeftCorner(bottomCornerFamily, bottomRadius) - } else { - shapeAppearanceModelBuilder - .setTopLeftCorner(CornerFamily.ROUNDED, cornerRadius) - .setBottomLeftCorner(CornerFamily.ROUNDED, cornerRadius) - .setTopRightCorner(topCornerFamily, topRadius) - .setBottomRightCorner(bottomCornerFamily, bottomRadius) - } - return shapeAppearanceModelBuilder.build() -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index 2f6ec15fde..373e9cfa68 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -16,7 +16,12 @@ package im.vector.app.features.home.room.detail.timeline.style +import android.content.res.Resources +import android.text.TextUtils +import android.view.View +import im.vector.app.R import im.vector.app.core.extensions.localDateTime +import im.vector.app.core.resources.LocaleProvider import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.session.Session @@ -31,6 +36,8 @@ import javax.inject.Inject class TimelineMessageLayoutFactory @Inject constructor(private val session: Session, private val layoutSettingsProvider: TimelineLayoutSettingsProvider, + private val localeProvider: LocaleProvider, + private val resources: Resources, private val vectorPreferences: VectorPreferences) { companion object { @@ -59,6 +66,15 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess ) } + private val cornerRadius: Float by lazy { + resources.getDimensionPixelSize(R.dimen.chat_bubble_corner_radius).toFloat() + } + + private val isRTL: Boolean by lazy { + val currentLocale = localeProvider.current() + TextUtils.getLayoutDirectionFromLocale(currentLocale) == View.LAYOUT_DIRECTION_RTL + } + fun create(params: TimelineItemFactoryParams): TimelineMessageLayout { val event = params.event val nextDisplayableEvent = params.nextDisplayableEvent @@ -94,13 +110,18 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess prevDisplayableEvent.root.senderId != event.root.senderId || prevDisplayableEvent.root.localDateTime().toLocalDate() != date.toLocalDate() + val cornersRadius = buildCornersRadius( + isIncoming = !isSentByMe, + isFirstFromThisSender = isFirstFromThisSender, + isLastFromThisSender = isLastFromThisSender + ) + val messageContent = event.getLastMessageContent() TimelineMessageLayout.Bubble( showAvatar = showInformation && !isSentByMe, showDisplayName = showInformation && !isSentByMe, isIncoming = !isSentByMe, - isFirstFromThisSender = isFirstFromThisSender, - isLastFromThisSender = isLastFromThisSender, + cornersRadius = cornersRadius, isPseudoBubble = messageContent.isPseudoBubble(), timestampAsOverlay = messageContent.timestampAsOverlay() ) @@ -112,15 +133,15 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess return messageLayout } - private fun MessageContent?.isPseudoBubble(): Boolean{ - if(this == null) return false - if(msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline() + private fun MessageContent?.isPseudoBubble(): Boolean { + if (this == null) return false + if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline() return this.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT } - private fun MessageContent?.timestampAsOverlay(): Boolean{ - if(this == null) return false - if(msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline() + private fun MessageContent?.timestampAsOverlay(): Boolean { + if (this == null) return false + if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline() return this.msgType in MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY } @@ -141,6 +162,24 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess ) } + private fun buildCornersRadius(isIncoming: Boolean, isFirstFromThisSender: Boolean, isLastFromThisSender: Boolean): TimelineMessageLayout.Bubble.CornersRadius { + return if ((isIncoming && !isRTL) || (!isIncoming && isRTL)) { + TimelineMessageLayout.Bubble.CornersRadius( + topStartRadius = if (isFirstFromThisSender) cornerRadius else 0f, + topEndRadius = cornerRadius, + bottomStartRadius = if (isLastFromThisSender) cornerRadius else 0f, + bottomEndRadius = cornerRadius + ) + } else { + TimelineMessageLayout.Bubble.CornersRadius( + topStartRadius = cornerRadius, + topEndRadius = if (isFirstFromThisSender) cornerRadius else 0f, + bottomStartRadius = cornerRadius, + bottomEndRadius = if (isLastFromThisSender) cornerRadius else 0f + ) + } + } + /** * Tiles type message never show the sender information (like verification request), so we should repeat it for next message * even if same sender diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index a843a408c9..7aea380123 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -48,7 +48,6 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri private var isIncoming: Boolean = false - private val cornerRadius = resources.getDimensionPixelSize(R.dimen.chat_bubble_corner_radius).toFloat() private val horizontalStubPadding = DimensionConverter(resources).dpToPx(12) private val verticalStubPadding = DimensionConverter(resources).dpToPx(4) @@ -116,7 +115,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } if (messageLayout.timestampAsOverlay) { views.messageOverlayView.isVisible = true - (views.messageOverlayView.background as? GradientDrawable)?.cornerRadii = messageLayout.cornerRadii(cornerRadius) + (views.messageOverlayView.background as? GradientDrawable)?.cornerRadii = messageLayout.cornersRadius.toFloatArray() } else { views.messageOverlayView.isVisible = false } @@ -125,7 +124,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } else { views.viewStubContainer.root.setPadding(horizontalStubPadding, verticalStubPadding, horizontalStubPadding, verticalStubPadding) } - if (messageLayout.isIncoming) { + if (isIncoming) { views.messageEndGuideline.updateLayoutParams<LayoutParams> { marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end) } @@ -142,18 +141,12 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } } - private fun TimelineMessageLayout.Bubble.cornerRadii(cornerRadius: Float): FloatArray { - val topRadius = if (isFirstFromThisSender) cornerRadius else 0f - val bottomRadius = if (isLastFromThisSender) cornerRadius else 0f - return if (isIncoming) { - floatArrayOf(topRadius, topRadius, cornerRadius, cornerRadius, cornerRadius, cornerRadius, bottomRadius, bottomRadius) - } else { - floatArrayOf(cornerRadius, cornerRadius, topRadius, topRadius, bottomRadius, bottomRadius, cornerRadius, cornerRadius) - } + private fun TimelineMessageLayout.Bubble.CornersRadius.toFloatArray(): FloatArray { + return floatArrayOf(topStartRadius, topStartRadius, topEndRadius, topEndRadius, bottomEndRadius, bottomEndRadius, bottomStartRadius, bottomStartRadius) } private fun updateDrawables(messageLayout: TimelineMessageLayout.Bubble) { - val shapeAppearanceModel = messageLayout.shapeAppearanceModel(cornerRadius) + val shapeAppearanceModel = messageLayout.cornersRadius.shapeAppearanceModel() bubbleDrawable.apply { this.shapeAppearanceModel = shapeAppearanceModel this.fillColor = if (messageLayout.isPseudoBubble) { From 38f1bbdab23bc4a6e153704a17c8741c7a66730b Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Mon, 31 Jan 2022 19:20:00 +0100 Subject: [PATCH 31/36] RTL: better support for some TextViews --- vector/src/main/res/layout/fragment_room_detail.xml | 2 ++ .../src/main/res/layout/item_bottom_sheet_message_preview.xml | 3 +++ vector/src/main/res/layout/item_room.xml | 2 ++ vector/src/main/res/layout/item_timeline_event_base.xml | 1 + vector/src/main/res/layout/item_timeline_event_base_noinfo.xml | 2 +- .../src/main/res/layout/item_timeline_event_default_stub.xml | 3 +-- vector/src/main/res/layout/item_timeline_event_notice_stub.xml | 3 +-- vector/src/main/res/layout/view_message_bubble.xml | 1 + 8 files changed, 12 insertions(+), 5 deletions(-) diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 65c106d6f3..ba28fda9ea 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -91,6 +91,7 @@ android:layout_marginEnd="8dp" android:ellipsize="end" android:maxLines="1" + android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.Vector.Widget.ActionBarTitle" app:layout_constraintBottom_toTopOf="@id/roomToolbarSubtitleView" app:layout_constraintEnd_toEndOf="parent" @@ -109,6 +110,7 @@ android:layout_marginEnd="8dp" android:ellipsize="end" android:maxLines="1" + android:textAlignment="viewStart" android:textAppearance="@style/TextAppearance.Vector.Widget.ActionBarSubTitle" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml index 95e6975803..08a8f5b27f 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml @@ -35,6 +35,7 @@ android:singleLine="true" android:textColor="?vctr_content_primary" android:textStyle="bold" + android:textAlignment="viewStart" app:layout_constraintEnd_toStartOf="@id/bottom_sheet_message_preview_timestamp" app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar" app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar" @@ -78,6 +79,7 @@ android:maxLines="3" android:textColor="?vctr_content_secondary" android:textIsSelectable="false" + android:textAlignment="viewStart" app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_body_details" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar" @@ -96,6 +98,7 @@ android:textColor="?vctr_content_tertiary" android:textIsSelectable="false" android:visibility="gone" + android:textAlignment="viewStart" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/bottom_sheet_message_preview_body" app:layout_constraintStart_toStartOf="@id/bottom_sheet_message_preview_body" diff --git a/vector/src/main/res/layout/item_room.xml b/vector/src/main/res/layout/item_room.xml index 41cc2cb02a..defff5eeeb 100644 --- a/vector/src/main/res/layout/item_room.xml +++ b/vector/src/main/res/layout/item_room.xml @@ -190,6 +190,7 @@ android:layout_height="wrap_content" android:layout_marginTop="3dp" android:layout_marginEnd="8dp" + android:textAlignment="viewStart" android:ellipsize="end" android:maxLines="2" android:textColor="?vctr_content_secondary" @@ -202,6 +203,7 @@ android:id="@+id/roomTypingView" style="@style/Widget.Vector.TextView.Body" android:layout_width="0dp" + android:textAlignment="viewStart" android:layout_height="wrap_content" android:layout_marginTop="3dp" android:layout_marginEnd="8dp" diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index bc9ed68232..3d0033fe59 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -34,6 +34,7 @@ android:layout_marginStart="8dp" android:layout_marginTop="4dp" android:layout_marginEnd="4dp" + android:textAlignment="viewStart" android:layout_toStartOf="@id/messageTimeView" android:layout_toEndOf="@id/messageStartGuideline" android:ellipsize="end" diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index 731642cabd..28a79de6bf 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -23,10 +23,10 @@ <FrameLayout android:id="@+id/viewStubContainer" + style="@style/TimelineContentStubContainerParams" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" - style="@style/TimelineContentStubContainerParams" android:layout_toEndOf="@id/messageStartGuideline"> <ViewStub diff --git a/vector/src/main/res/layout/item_timeline_event_default_stub.xml b/vector/src/main/res/layout/item_timeline_event_default_stub.xml index 000429cc68..8225f52bb8 100644 --- a/vector/src/main/res/layout/item_timeline_event_default_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_default_stub.xml @@ -19,9 +19,8 @@ <TextView android:id="@+id/itemDefaultTextView" style="@style/Widget.Vector.TextView.Body" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="top" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" diff --git a/vector/src/main/res/layout/item_timeline_event_notice_stub.xml b/vector/src/main/res/layout/item_timeline_event_notice_stub.xml index e6ccdc4256..9d3a3c4098 100644 --- a/vector/src/main/res/layout/item_timeline_event_notice_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_notice_stub.xml @@ -19,9 +19,8 @@ <TextView android:id="@+id/itemNoticeTextView" style="@style/Widget.Vector.TextView.Body" - android:layout_width="match_parent" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="top" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" diff --git a/vector/src/main/res/layout/view_message_bubble.xml b/vector/src/main/res/layout/view_message_bubble.xml index 1ffb358d4e..6e316f7979 100644 --- a/vector/src/main/res/layout/view_message_bubble.xml +++ b/vector/src/main/res/layout/view_message_bubble.xml @@ -29,6 +29,7 @@ <TextView android:id="@+id/messageMemberNameView" style="@style/Widget.Vector.TextView.Subtitle" + android:textAlignment="viewStart" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" From 50810065a217403939c0340f9c32e3ec669ee612 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Tue, 1 Feb 2022 20:19:35 +0100 Subject: [PATCH 32/36] Bubbles: update media sizing (including LocationItem) --- .../timeline/factory/MessageItemFactory.kt | 6 ++++- .../helper/TimelineMediaSizeProvider.kt | 15 ++++++++--- .../timeline/item/MessageLocationItem.kt | 25 +++++++++++++++++-- .../RoomWidgetPermissionBottomSheet.kt | 1 - .../item_timeline_event_location_stub.xml | 23 +++++++++++------ 5 files changed, 56 insertions(+), 14 deletions(-) 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 cedcb66ef3..eda0fb925c 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 @@ -25,6 +25,8 @@ import android.text.style.AbsoluteSizeSpan import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.view.View +import android.view.WindowManager +import android.view.WindowMetrics import dagger.Lazy import im.vector.app.R import im.vector.app.core.epoxy.ClickListener @@ -199,7 +201,7 @@ class MessageItemFactory @Inject constructor( informationData: MessageInformationData, highlight: Boolean, attributes: AbsMessageItem.Attributes): MessageLocationItem? { - val width = resources.displayMetrics.widthPixels - dimensionConverter.dpToPx(60) + val width = timelineMediaSizeProvider.getMaxSize().first val height = dimensionConverter.dpToPx(200) val locationUrl = locationContent.toLocationData()?.let { @@ -209,6 +211,8 @@ class MessageItemFactory @Inject constructor( return MessageLocationItem_() .attributes(attributes) .locationUrl(locationUrl) + .mapWidth(width) + .mapHeight(height) .userId(informationData.senderId) .locationPinProvider(locationPinProvider) .highlighted(highlight) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineMediaSizeProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineMediaSizeProvider.kt index 9ec61e6054..53c2f6c0d4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineMediaSizeProvider.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineMediaSizeProvider.kt @@ -16,13 +16,17 @@ package im.vector.app.features.home.room.detail.timeline.helper +import android.content.res.Resources import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.scopes.ActivityScoped +import im.vector.app.R +import im.vector.app.features.settings.VectorPreferences import javax.inject.Inject import kotlin.math.roundToInt @ActivityScoped -class TimelineMediaSizeProvider @Inject constructor() { +class TimelineMediaSizeProvider @Inject constructor(private val resources: Resources, + private val vectorPreferences: VectorPreferences) { var recyclerView: RecyclerView? = null private var cachedSize: Pair<Int, Int>? = null @@ -41,9 +45,14 @@ class TimelineMediaSizeProvider @Inject constructor() { maxImageWidth = (width * 0.7f).roundToInt() maxImageHeight = (height * 0.5f).roundToInt() } else { - maxImageWidth = (width * 0.5f).roundToInt() + maxImageWidth = (width * 0.7f).roundToInt() maxImageHeight = (height * 0.7f).roundToInt() } - return Pair(maxImageWidth, maxImageHeight) + return if (vectorPreferences.useMessageBubblesLayout()) { + val bubbleMaxImageWidth = maxImageWidth.coerceAtMost(resources.getDimensionPixelSize(R.dimen.chat_bubble_fixed_size)) + Pair(bubbleMaxImageWidth, maxImageHeight) + } else { + Pair(maxImageWidth, maxImageHeight) + } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt index 11faad8f13..6632b4ef30 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt @@ -17,12 +17,16 @@ package im.vector.app.features.home.room.detail.timeline.item import android.widget.ImageView +import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.load.resource.bitmap.RoundedCorners import im.vector.app.R import im.vector.app.core.glide.GlideApp +import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() { @@ -33,6 +37,12 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>( @EpoxyAttribute var userId: String? = null + @EpoxyAttribute + var mapWidth: Int = 0 + + @EpoxyAttribute + var mapHeight: Int = 0 + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var locationPinProvider: LocationPinProvider? = null @@ -41,9 +51,20 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>( renderSendState(holder.view, null) val location = locationUrl ?: return val locationOwnerId = userId ?: return + val messageLayout = attributes.informationData.messageLayout + val dimensionConverter = DimensionConverter(holder.view.resources) + val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) { + messageLayout.cornersRadius.granularRoundedCorners() + } else { + RoundedCorners(dimensionConverter.dpToPx(8)) + } + holder.staticMapImageView.updateLayoutParams { + width = mapWidth + height = mapHeight + } GlideApp.with(holder.staticMapImageView) .load(location) - .apply(RequestOptions.centerCropTransform()) + .transform(imageCornerTransformation) .into(holder.staticMapImageView) locationPinProvider?.create(locationOwnerId) { pinDrawable -> diff --git a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt index 58cfebba94..91371b1f73 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/permissions/RoomWidgetPermissionBottomSheet.kt @@ -53,7 +53,6 @@ class RoomWidgetPermissionBottomSheet : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupViews() } diff --git a/vector/src/main/res/layout/item_timeline_event_location_stub.xml b/vector/src/main/res/layout/item_timeline_event_location_stub.xml index 316470b5f1..77c7b932ce 100644 --- a/vector/src/main/res/layout/item_timeline_event_location_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_location_stub.xml @@ -1,16 +1,21 @@ <?xml version="1.0" encoding="utf-8"?> -<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" +<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent" - android:layout_height="wrap_content" - app:cardCornerRadius="8dp"> + android:layout_height="wrap_content"> + <!-- Size will be overrode --> <ImageView android:id="@+id/staticMapImageView" - android:layout_width="match_parent" + android:layout_width="300dp" android:layout_height="200dp" - android:contentDescription="@string/a11y_static_map_image" /> + android:contentDescription="@string/a11y_static_map_image" + android:scaleType="centerCrop" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@tools:sample/backgrounds/scenic" /> <ImageView android:id="@+id/staticMapPinImageView" @@ -19,6 +24,10 @@ android:layout_gravity="center" android:layout_marginBottom="28dp" android:importantForAccessibility="no" - android:src="@drawable/bg_map_user_pin" /> + android:src="@drawable/bg_map_user_pin" + app:layout_constraintBottom_toBottomOf="@id/staticMapImageView" + app:layout_constraintEnd_toEndOf="@id/staticMapImageView" + app:layout_constraintStart_toStartOf="@id/staticMapImageView" + app:layout_constraintTop_toTopOf="@id/staticMapImageView" /> -</com.google.android.material.card.MaterialCardView> +</androidx.constraintlayout.widget.ConstraintLayout> From cfda76b2d4904ab2691ed29f71ac2e1e3967f162 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Tue, 1 Feb 2022 20:31:09 +0100 Subject: [PATCH 33/36] Timeline: avoid overflow on getViewType --- .../home/room/detail/timeline/item/BaseEventItem.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt index 5c56ad4e76..8ea761830a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -26,8 +26,6 @@ import im.vector.app.R 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.utils.DimensionConverter -import timber.log.Timber /** * Children must override getViewType() @@ -43,15 +41,14 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H> final override fun getViewType(): Int { // This makes sure we have a unique integer for the combination of layout and ViewStubId. - return pairingFunction(layout, getViewStubId()).also { - Timber.v("GetViewType: for ${javaClass.canonicalName} $it with layout:$layout and stubId:${getViewStubId()}") - } + val pairingResult = pairingFunction(layout.toLong(), getViewStubId().toLong()) + return (pairingResult - Int.MAX_VALUE).toInt() } abstract fun getViewStubId(): Int // Szudzik function - private fun pairingFunction(a: Int, b: Int): Int { + private fun pairingFunction(a: Long, b: Long): Long { return if (a >= b) a * a + a + b else a + b * b } From 9e0086742c2ab129aa5b2821c748448e780dc857 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Wed, 2 Feb 2022 19:56:21 +0100 Subject: [PATCH 34/36] Bubbles: clean up after review --- .../home/room/detail/timeline/factory/MessageItemFactory.kt | 2 -- .../timeline/helper/ContentDownloadStateTrackerBinder.kt | 4 +--- .../home/room/detail/timeline/item/AbsBaseMessageItem.kt | 2 +- .../home/room/detail/timeline/item/MessageFileItem.kt | 4 ++-- .../home/room/detail/timeline/item/MessageImageVideoItem.kt | 1 - .../home/room/detail/timeline/item/MessageTextItem.kt | 2 +- .../home/room/detail/timeline/item/MessageVoiceItem.kt | 5 ++--- .../detail/timeline/style/TimelineMessageLayoutFactory.kt | 6 ++++-- .../home/room/detail/timeline/url/PreviewUrlView.kt | 4 +--- .../home/room/detail/timeline/view/MessageBubbleView.kt | 2 +- .../detail/timeline/view/TimelineMessageLayoutRenderer.kt | 2 +- .../im/vector/app/features/settings/VectorPreferences.kt | 4 ++-- vector/src/main/res/layout/view_message_bubble.xml | 2 +- vector/src/main/res/layout/view_thread_room_summary.xml | 2 +- 14 files changed, 18 insertions(+), 24 deletions(-) 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 51fd8d81bd..7789ae0e76 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 @@ -25,8 +25,6 @@ import android.text.style.AbsoluteSizeSpan import android.text.style.ClickableSpan import android.text.style.ForegroundColorSpan import android.view.View -import android.view.WindowManager -import android.view.WindowMetrics import dagger.Lazy import im.vector.app.R import im.vector.app.core.epoxy.ClickListener diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt index e4405570a6..8f5f80c834 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt @@ -22,14 +22,12 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import dagger.hilt.android.scopes.ActivityScoped import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder -import im.vector.app.core.error.ErrorFormatter -import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker import javax.inject.Inject @ActivityScoped -class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder){ +class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) { private val updateListeners = mutableMapOf<String, ContentDownloadUpdater>() 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 ac9ef1522d..9621b1c2f9 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 @@ -99,7 +99,7 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem holder.view.onClick(baseAttributes.itemClickListener) holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener) - (holder.view as? TimelineMessageLayoutRenderer)?.render(baseAttributes.informationData.messageLayout) + (holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout) } override fun unbind(holder: H) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt index e736c0e2da..8b6899daee 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -84,9 +84,9 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() { } } // holder.view.setOnClickListener(clickListener) - val backgroundTint = if(attributes.informationData.messageLayout is TimelineMessageLayout.Bubble){ + val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) { Color.TRANSPARENT - }else { + } else { ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary) } holder.mainLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint) 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 6f801d9eb7..8485c40ef9 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 @@ -23,7 +23,6 @@ import androidx.core.view.ViewCompat import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners import com.bumptech.glide.load.resource.bitmap.RoundedCorners import im.vector.app.R import im.vector.app.core.epoxy.ClickListener diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt index ed9bc87834..bc9e4a7ff1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageTextItem.kt @@ -80,7 +80,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() { safePreviewUrlRetriever.addListener(attributes.informationData.eventId, previewUrlViewUpdater) } holder.previewUrlView.delegate = previewUrlCallback - holder.previewUrlView.render(attributes.informationData.messageLayout) + holder.previewUrlView.renderMessageLayout(attributes.informationData.messageLayout) if (useBigFont) { holder.messageView.textSize = 44F diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt index 1058e3c1f9..e9f728d976 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -23,7 +23,6 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.TextView -import androidx.core.content.ContextCompat import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass @@ -86,9 +85,9 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() { } } - val backgroundTint = if(attributes.informationData.messageLayout is TimelineMessageLayout.Bubble){ + val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) { Color.TRANSPARENT - }else { + } else { ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary) } holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index 373e9cfa68..bded4505b5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -41,7 +41,7 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess private val vectorPreferences: VectorPreferences) { companion object { - // Can't be rendered in bubbles, so get back to default layout + // Can be rendered in bubbles, other types will fallback to default private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf( EventType.MESSAGE, EventType.POLL_START, @@ -162,7 +162,9 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess ) } - private fun buildCornersRadius(isIncoming: Boolean, isFirstFromThisSender: Boolean, isLastFromThisSender: Boolean): TimelineMessageLayout.Bubble.CornersRadius { + private fun buildCornersRadius(isIncoming: Boolean, + isFirstFromThisSender: Boolean, + isLastFromThisSender: Boolean): TimelineMessageLayout.Bubble.CornersRadius { return if ((isIncoming && !isRTL) || (!isIncoming && isRTL)) { TimelineMessageLayout.Bubble.CornersRadius( topStartRadius = if (isFirstFromThisSender) cornerRadius else 0f, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt index 905d420463..bb306c2016 100755 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/url/PreviewUrlView.kt @@ -25,7 +25,6 @@ import androidx.core.view.isVisible import com.google.android.material.card.MaterialCardView import im.vector.app.R import im.vector.app.core.extensions.setTextOrHide -import im.vector.app.core.glide.GlideApp import im.vector.app.core.utils.DimensionConverter import im.vector.app.databinding.ViewUrlPreviewBinding import im.vector.app.features.home.room.detail.timeline.TimelineEventController @@ -33,7 +32,6 @@ import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLay import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.themes.ThemeUtils -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.media.PreviewUrlData /** @@ -81,7 +79,7 @@ class PreviewUrlView @JvmOverloads constructor( } } - override fun render(messageLayout: TimelineMessageLayout) { + override fun renderMessageLayout(messageLayout: TimelineMessageLayout) { when (messageLayout) { is TimelineMessageLayout.Default -> { val backgroundColor = ThemeUtils.getColor(context, R.attr.vctr_system) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index 0f4db38028..de40a681b3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -94,7 +94,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri } } - override fun render(messageLayout: TimelineMessageLayout) { + override fun renderMessageLayout(messageLayout: TimelineMessageLayout) { if (messageLayout !is TimelineMessageLayout.Bubble) { Timber.v("Can't render messageLayout $messageLayout") return diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/TimelineMessageLayoutRenderer.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/TimelineMessageLayoutRenderer.kt index a339119f46..0c42662801 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/TimelineMessageLayoutRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/TimelineMessageLayoutRenderer.kt @@ -19,5 +19,5 @@ package im.vector.app.features.home.room.detail.timeline.view import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout interface TimelineMessageLayoutRenderer { - fun render(messageLayout: TimelineMessageLayout) + fun renderMessageLayout(messageLayout: TimelineMessageLayout) } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index d73b87d42c..bdf9f284b8 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -862,9 +862,9 @@ class VectorPreferences @Inject constructor(private val context: Context) { } /** - * Tells if the emoji keyboard button should be visible or not. + * Tells if the timeline messages should be shown in a bubble or not. * - * @return true to show emoji keyboard button. + * @return true to show timeline message in bubble. */ fun useMessageBubblesLayout(): Boolean { return defaultPrefs.getBoolean(SETTINGS_INTERFACE_BUBBLE_KEY, false) diff --git a/vector/src/main/res/layout/view_message_bubble.xml b/vector/src/main/res/layout/view_message_bubble.xml index b86f52bf63..7e4d3e8f7d 100644 --- a/vector/src/main/res/layout/view_message_bubble.xml +++ b/vector/src/main/res/layout/view_message_bubble.xml @@ -200,7 +200,7 @@ android:layout_height="wrap_content" android:layout_below="@id/informationBottom" android:layout_toStartOf="@id/messageEndGuideline" - android:layout_toEndOf="@+id/messageStartGuideline" + android:layout_toEndOf="@id/messageStartGuideline" android:contentDescription="@string/room_threads_filter"> <androidx.constraintlayout.widget.ConstraintLayout diff --git a/vector/src/main/res/layout/view_thread_room_summary.xml b/vector/src/main/res/layout/view_thread_room_summary.xml index c326d8cbcc..0f184edef3 100644 --- a/vector/src/main/res/layout/view_thread_room_summary.xml +++ b/vector/src/main/res/layout/view_thread_room_summary.xml @@ -55,5 +55,5 @@ app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toEndOf="@id/messageThreadSummaryAvatarImageView" app:layout_constraintTop_toTopOf="parent" - tools:text="Pouet" /> + tools:text="@sample/messages.json/data/message" /> </merge> From 04234318e5bb52f3ec0e95ff87826229d41d2ba9 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Thu, 3 Feb 2022 17:53:25 +0100 Subject: [PATCH 35/36] Bubble: get LayoutDirection (RTL) from current Locale --- library/ui-styles/src/main/res/values-ldrtl/bools.xml | 6 ------ library/ui-styles/src/main/res/values/bools.xml | 2 -- .../java/im/vector/app/core/resources/LocaleProvider.kt | 6 ++++++ .../detail/timeline/style/TimelineMessageLayoutFactory.kt | 4 ++-- .../home/room/detail/timeline/view/MessageBubbleView.kt | 5 ++--- .../java/im/vector/app/features/location/UrlMapProvider.kt | 6 ++++-- 6 files changed, 14 insertions(+), 15 deletions(-) delete mode 100644 library/ui-styles/src/main/res/values-ldrtl/bools.xml diff --git a/library/ui-styles/src/main/res/values-ldrtl/bools.xml b/library/ui-styles/src/main/res/values-ldrtl/bools.xml deleted file mode 100644 index 27b280985f..0000000000 --- a/library/ui-styles/src/main/res/values-ldrtl/bools.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - - <bool name="is_rtl">true</bool> - -</resources> \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/bools.xml b/library/ui-styles/src/main/res/values/bools.xml index 9966999f28..93d5f925af 100644 --- a/library/ui-styles/src/main/res/values/bools.xml +++ b/library/ui-styles/src/main/res/values/bools.xml @@ -4,6 +4,4 @@ <!-- Created to detect what has to be implemented (especially in the settings) --> <bool name="false_not_implemented">false</bool> - <bool name="is_rtl">false</bool> - </resources> \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/core/resources/LocaleProvider.kt b/vector/src/main/java/im/vector/app/core/resources/LocaleProvider.kt index 6a9d434aea..fdb5f21b61 100644 --- a/vector/src/main/java/im/vector/app/core/resources/LocaleProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/LocaleProvider.kt @@ -17,6 +17,8 @@ package im.vector.app.core.resources import android.content.res.Resources +import android.text.TextUtils +import android.view.View import androidx.core.os.ConfigurationCompat import java.util.Locale import javax.inject.Inject @@ -29,3 +31,7 @@ class LocaleProvider @Inject constructor(private val resources: Resources) { } fun LocaleProvider.isEnglishSpeaking() = current().language.startsWith("en") + +fun LocaleProvider.getLayoutDirectionFromCurrentLocale() = TextUtils.getLayoutDirectionFromLocale(current()) + +fun LocaleProvider.isRTL() = getLayoutDirectionFromCurrentLocale() == View.LAYOUT_DIRECTION_RTL diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index bded4505b5..b24ab81997 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -22,6 +22,7 @@ import android.view.View import im.vector.app.R import im.vector.app.core.extensions.localDateTime 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 org.matrix.android.sdk.api.session.Session @@ -71,8 +72,7 @@ class TimelineMessageLayoutFactory @Inject constructor(private val session: Sess } private val isRTL: Boolean by lazy { - val currentLocale = localeProvider.current() - TextUtils.getLayoutDirectionFromLocale(currentLocale) == View.LAYOUT_DIRECTION_RTL + localeProvider.isRTL() } fun create(params: TimelineItemFactoryParams): TimelineMessageLayout { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt index de40a681b3..422dfb0dbd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -21,7 +21,6 @@ import android.content.res.ColorStateList import android.graphics.Color import android.graphics.drawable.GradientDrawable import android.graphics.drawable.RippleDrawable -import android.text.TextUtils import android.util.AttributeSet import android.view.View import android.view.ViewOutlineProvider @@ -35,6 +34,7 @@ import androidx.core.view.updateLayoutParams 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.utils.DimensionConverter import im.vector.app.databinding.ViewMessageBubbleBinding import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout @@ -65,8 +65,7 @@ class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: Attri override fun onFinishInflate() { super.onFinishInflate() views = ViewMessageBubbleBinding.bind(this) - val currentLocale = LocaleProvider(resources).current() - val currentLayoutDirection = TextUtils.getLayoutDirectionFromLocale(currentLocale) + val currentLayoutDirection = LocaleProvider(resources).getLayoutDirectionFromCurrentLocale() val layoutDirectionToSet = if (isIncoming) { currentLayoutDirection } else { diff --git a/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt b/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt index 76d44f5ece..8e64dc959e 100644 --- a/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt +++ b/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt @@ -19,10 +19,12 @@ package im.vector.app.features.location import android.content.res.Resources import im.vector.app.BuildConfig import im.vector.app.R +import im.vector.app.core.resources.LocaleProvider +import im.vector.app.core.resources.isRTL import javax.inject.Inject class UrlMapProvider @Inject constructor( - private val resources: Resources + private val localeProvider: LocaleProvider ) { private val keyParam = "?key=${BuildConfig.mapTilerKey}" @@ -49,7 +51,7 @@ class UrlMapProvider @Inject constructor( append(height) append(".png") append(keyParam) - if (!resources.getBoolean(R.bool.is_rtl)) { + if (!localeProvider.isRTL()) { // On LTR languages we want the legal mentions to be displayed on the bottom left of the image append("&attribution=bottomleft") } From 02de6369554218dfc626a5c19d82dc0e4c89cbc2 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@element.io> Date: Thu, 3 Feb 2022 17:55:44 +0100 Subject: [PATCH 36/36] Bubbles: add CHANGELOG file --- changelog.d/4937.feature | 1 + .../home/room/detail/timeline/factory/MessageItemFactory.kt | 1 - .../room/detail/timeline/style/TimelineMessageLayoutFactory.kt | 2 -- .../main/java/im/vector/app/features/location/UrlMapProvider.kt | 2 -- 4 files changed, 1 insertion(+), 5 deletions(-) create mode 100644 changelog.d/4937.feature diff --git a/changelog.d/4937.feature b/changelog.d/4937.feature new file mode 100644 index 0000000000..8f2a3f5bc6 --- /dev/null +++ b/changelog.d/4937.feature @@ -0,0 +1 @@ +Support message bubbles in timeline. \ No newline at end of file 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 2ad36abbde..0c836748c8 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 @@ -16,7 +16,6 @@ package im.vector.app.features.home.room.detail.timeline.factory -import android.content.res.Resources import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt index b24ab81997..8c1c308bb5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/style/TimelineMessageLayoutFactory.kt @@ -17,8 +17,6 @@ package im.vector.app.features.home.room.detail.timeline.style import android.content.res.Resources -import android.text.TextUtils -import android.view.View import im.vector.app.R import im.vector.app.core.extensions.localDateTime import im.vector.app.core.resources.LocaleProvider diff --git a/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt b/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt index 8e64dc959e..c9d9cf8112 100644 --- a/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt +++ b/vector/src/main/java/im/vector/app/features/location/UrlMapProvider.kt @@ -16,9 +16,7 @@ package im.vector.app.features.location -import android.content.res.Resources import im.vector.app.BuildConfig -import im.vector.app.R import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.isRTL import javax.inject.Inject