From af542a8243ff562791f8886cb438159bd314a253 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 11 Jan 2022 15:38:58 +0100 Subject: [PATCH] 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(), 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() - 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 : AbsBaseMessageItem 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 : AbsBaseMessageItem 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 : AbsBaseMessageItem 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? = 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 + } + } +}