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

View file

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

View file

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

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.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 {

View file

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

View file

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

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