Bubbles: start adding "theming" mechanism

This commit is contained in:
ganfra 2022-01-11 15:38:58 +01:00
parent f7c9b36cef
commit af542a8243
10 changed files with 212 additions and 49 deletions

View file

@ -108,6 +108,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
val informationData = messageInformationDataFactory.create(params) val informationData = messageInformationDataFactory.create(params)
val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, params.callback) val attributes = attributesFactory.create(event.root.content.toModel<EncryptedEventContent>(), informationData, params.callback)
return MessageTextItem_() return MessageTextItem_()
.layout(informationData.messageLayout.layoutRes)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(params.isHighlighted) .highlighted(params.isHighlighted)
.attributes(attributes) .attributes(attributes)

View file

@ -154,7 +154,7 @@ class MessageItemFactory @Inject constructor(
// val all = event.root.toContent() // val all = event.root.toContent()
// val ev = all.toModel<Event>() // val ev = all.toModel<Event>()
return when (messageContent) { val messageItem = when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageInfoContent -> buildImageMessageItem(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) is MessagePollContent -> buildPollContent(messageContent, informationData, highlight, callback, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
} }
return messageItem?.apply {
layout(informationData.messageLayout.layoutRes)
}
} }
private fun buildPollContent(pollContent: MessagePollContent, private fun buildPollContent(pollContent: MessagePollContent,
@ -389,13 +392,6 @@ class MessageItemFactory @Inject constructor(
allowNonMxcUrls = informationData.sendState.isSending() allowNonMxcUrls = informationData.sendState.isSending()
) )
return MessageImageVideoItem_() 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) .attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.imageContentRenderer(imageContentRenderer) .imageContentRenderer(imageContentRenderer)
@ -517,13 +513,6 @@ class MessageItemFactory @Inject constructor(
linkifiedBody linkifiedBody
}.toEpoxyCharSequence() }.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())) .useBigFont(linkifiedBody.length <= MAX_NUMBER_OF_EMOJI_FOR_BIG_FONT * 2 && containsOnlyEmojis(linkifiedBody.toString()))
.bindingOptions(bindingOptions) .bindingOptions(bindingOptions)
.searchForPills(isFormatted) .searchForPills(isFormatted)

View file

@ -17,14 +17,22 @@
package im.vector.app.features.home.room.detail.timeline.helper package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.utils.DimensionConverter 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 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 { val leftGuideline: Int by lazy {
dimensionConverter.dpToPx(avatarStyle.avatarSizeDP + 4) dimensionConverter.dpToPx(avatarStyle.avatarSizeDP + avatarStyle.marginDP)
} }
val avatarSize: Int by lazy { val avatarSize: Int by lazy {
@ -33,12 +41,12 @@ class AvatarSizeProvider @Inject constructor(private val dimensionConverter: Dim
companion object { companion object {
enum class AvatarStyle(val avatarSizeDP: Int) { enum class AvatarStyle(val avatarSizeDP: Int, val marginDP: Int) {
BIG(50), BIG(50, 8),
MEDIUM(40), MEDIUM(40, 8),
SMALL(30), SMALL(30, 8),
X_SMALL(28), BUBBLE(28, 4),
NONE(0) NONE(0, 8)
} }
} }
} }

View file

@ -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.ReactionInfoData
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData 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.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.crypto.VerificationState
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session 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.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent 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.hasBeenEdited
import org.matrix.android.sdk.api.session.room.timeline.isEdition
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import javax.inject.Inject import javax.inject.Inject
@ -51,8 +50,7 @@ import javax.inject.Inject
*/ */
class MessageInformationDataFactory @Inject constructor(private val session: Session, class MessageInformationDataFactory @Inject constructor(private val session: Session,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val visibilityHelper: TimelineEventVisibilityHelper, private val messageLayoutFactory: TimelineMessageLayoutFactory) {
private val vectorPreferences: VectorPreferences) {
fun create(params: TimelineItemFactoryParams): MessageInformationData { fun create(params: TimelineItemFactoryParams): MessageInformationData {
val event = params.event val event = params.event
@ -66,21 +64,9 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
val nextDate = nextDisplayableEvent?.root?.localDateTime() val nextDate = nextDisplayableEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate() val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
?: false
val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator 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 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 time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
val e2eDecoration = getE2EDecoration(roomSummary, event) val e2eDecoration = getE2EDecoration(roomSummary, event)
@ -95,6 +81,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
SendStateDecoration.NONE SendStateDecoration.NONE
} }
val messageLayout = messageLayoutFactory.create(params)
return MessageInformationData( return MessageInformationData(
eventId = eventId, eventId = eventId,
senderId = event.root.senderId ?: "", senderId = event.root.senderId ?: "",
@ -103,9 +91,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
ageLocalTS = event.root.ageLocalTs, ageLocalTS = event.root.ageLocalTs,
avatarUrl = event.senderInfo.avatarUrl, avatarUrl = event.senderInfo.avatarUrl,
memberName = event.senderInfo.disambiguatedDisplayName, memberName = event.senderInfo.disambiguatedDisplayName,
showAvatar = showInformation, messageLayout = messageLayout,
showDisplayName = showInformation,
showTimestamp = true,
orderedReactionList = event.annotations?.reactionsSummary orderedReactionList = event.annotations?.reactionsSummary
// ?.filter { isSingleEmoji(it.key) } // ?.filter { isSingleEmoji(it.key) }
?.map { ?.map {

View file

@ -62,7 +62,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
if (attributes.informationData.showAvatar) { if (attributes.informationData.messageLayout.showAvatar) {
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply { holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
height = attributes.avatarSize height = attributes.avatarSize
width = attributes.avatarSize width = attributes.avatarSize
@ -76,7 +76,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
holder.avatarImageView.setOnLongClickListener(null) holder.avatarImageView.setOnLongClickListener(null)
holder.avatarImageView.isVisible = false holder.avatarImageView.isVisible = false
} }
if (attributes.informationData.showDisplayName) { if (attributes.informationData.messageLayout.showDisplayName) {
holder.memberNameView.isVisible = true holder.memberNameView.isVisible = true
holder.memberNameView.text = attributes.informationData.memberName holder.memberNameView.text = attributes.informationData.memberName
holder.memberNameView.setTextColor(attributes.getMemberNameColor()) holder.memberNameView.setTextColor(attributes.getMemberNameColor())
@ -87,7 +87,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
holder.memberNameView.setOnLongClickListener(null) holder.memberNameView.setOnLongClickListener(null)
holder.memberNameView.isVisible = false holder.memberNameView.isVisible = false
} }
if (attributes.informationData.showTimestamp) { if (attributes.informationData.messageLayout.showTimestamp) {
holder.timeView.isVisible = true holder.timeView.isVisible = true
holder.timeView.text = attributes.informationData.time holder.timeView.text = attributes.informationData.time
} else { } else {

View file

@ -17,6 +17,7 @@
package im.vector.app.features.home.room.detail.timeline.item package im.vector.app.features.home.room.detail.timeline.item
import android.os.Parcelable import android.os.Parcelable
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.crypto.VerificationState import org.matrix.android.sdk.api.crypto.VerificationState
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
@ -31,9 +32,7 @@ data class MessageInformationData(
val ageLocalTS: Long?, val ageLocalTS: Long?,
val avatarUrl: String?, val avatarUrl: String?,
val memberName: CharSequence? = null, val memberName: CharSequence? = null,
val showAvatar: Boolean, val messageLayout: TimelineMessageLayout,
val showDisplayName: Boolean,
val showTimestamp: Boolean,
/*List of reactions (emoji,count,isSelected)*/ /*List of reactions (emoji,count,isSelected)*/
val orderedReactionList: List<ReactionInfoData>? = null, val orderedReactionList: List<ReactionInfoData>? = null,
val pollResponseAggregatedSummary: PollResponseData? = null, val pollResponseAggregatedSummary: PollResponseData? = null,

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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
}
}
}