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/res/drawable/bg_voice_playback.xml b/library/ui-styles/src/main/res/drawable/bg_media_pill.xml similarity index 82% 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 @@ - - - - + + - - + 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 @@ - - - - true - - \ 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 @@ false - false - \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/colors.xml b/library/ui-styles/src/main/res/values/colors.xml index 9df2794a1a..ca6f6d3142 100644 --- a/library/ui-styles/src/main/res/values/colors.xml +++ b/library/ui-styles/src/main/res/values/colors.xml @@ -137,4 +137,5 @@ @color/palette_gray_100 @color/palette_gray_450 + 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 @@ + + + + + + #E8EDF4 + #21262C + + #E7F8F3 + #133A34 + \ No newline at end of file diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml index 7e79218281..be57f75dc8 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 @@ 72dp 16dp + 32dp + 40dp 60dp @@ -42,6 +44,7 @@ 8dp + 160dp 24dp 48dp @@ -52,6 +55,12 @@ 52dp 1dp + + 28dp + 62dp + 300dp + 12dp + 0.05 0.95 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..f7a877e3ed --- /dev/null +++ b/library/ui-styles/src/main/res/values/stylable_message_bubble.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + 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 @@ 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..3bd3543de2 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,23 @@ + + + + \ 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 54b8fd3200..b1d95c5439 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 @@ @color/vctr_waiting_background_color_dark @color/vctr_chat_effect_snow_background_dark @color/element_system_dark + @color/vctr_message_bubble_inbound_dark + @color/vctr_message_bubble_outbound_dark #61708B 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 ee3f41635e..dba39c97ca 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 @@ @color/vctr_waiting_background_color_light @color/vctr_chat_effect_snow_background_light @color/element_background_light + @color/vctr_message_bubble_inbound_light + @color/vctr_message_bubble_outbound_light #61708B 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 6c66ec9833..dfb0915566 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 @@ -57,7 +57,7 @@ internal class RealmSessionStoreMigration @Inject constructor( ) : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 23L + const val SESSION_STORE_SCHEMA_VERSION = 24L } /** @@ -93,6 +93,7 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion <= 20) migrateTo21(realm) if (oldVersion <= 21) migrateTo22(realm) if (oldVersion <= 22) migrateTo23(realm) + if (oldVersion <= 23) migrateTo24(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -479,4 +480,13 @@ internal class RealmSessionStoreMigration @Inject constructor( } ?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity) } + + private fun migrateTo24(realm: DynamicRealm) { + Timber.d("Step 23 -> 24") + 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/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/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 1efc0377d3..2ac592797c 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 @@ -382,20 +382,28 @@ 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( - timelineEvent = it, - highlightedEventId = partialState.highlightedEventId, - isFromThreadTimeline = partialState.isFromThreadTimeline(), - rootThreadEventId = partialState.rootThreadEventId) - } // 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( + timelineEvent = it, + highlightedEventId = partialState.highlightedEventId, + isFromThreadTimeline = partialState.isFromThreadTimeline(), + rootThreadEventId = partialState.rootThreadEventId) + } + val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull { + timelineEventVisibilityHelper.shouldShowEvent( + timelineEvent = it, + highlightedEventId = partialState.highlightedEventId, + isFromThreadTimeline = partialState.isFromThreadTimeline(), + rootThreadEventId = partialState.rootThreadEventId) + } val timelineEventsGroup = timelineEventsGroups.getOrNull(event) 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/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index 413ceb6380..bc2497392c 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 @@ -113,6 +113,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat callback = params.callback, threadDetails = threadDetails) 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 77bf5970af..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 @@ -44,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 @@ -66,7 +63,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 @@ -79,7 +75,6 @@ import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.settings.VectorPreferences import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence 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 @@ -134,7 +129,6 @@ class MessageItemFactory @Inject constructor( private val locationPinProvider: LocationPinProvider, private val vectorPreferences: VectorPreferences, private val urlMapProvider: UrlMapProvider, - private val resources: Resources ) { // TODO inject this properly? @@ -181,7 +175,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) @@ -206,13 +200,16 @@ class MessageItemFactory @Inject constructor( } else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } + return messageItem?.apply { + layout(informationData.messageLayout.layoutRes) + } } private fun buildLocationItem(locationContent: MessageLocationContent, 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 { @@ -224,6 +221,8 @@ class MessageItemFactory @Inject constructor( return MessageLocationItem_() .attributes(attributes) .locationUrl(locationUrl) + .mapWidth(width) + .mapHeight(height) .userId(userId) .locationPinProvider(locationPinProvider) .highlighted(highlight) @@ -526,46 +525,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, @@ -598,24 +573,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 { @@ -721,6 +678,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/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt index 8479d6b589..46ae01a794 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/AvatarSizeProvider.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AvatarSizeProvider.kt index 5fc5deb407..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.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 + 8) + dimensionConverter.dpToPx(avatarStyle.avatarSizeDP + avatarStyle.marginDP) } val avatarSize: Int by lazy { @@ -33,11 +41,12 @@ class AvatarSizeProvider @Inject constructor(private val dimensionConverter: Dim companion object { - enum class AvatarStyle(val avatarSizeDP: Int) { - BIG(50), - MEDIUM(40), - SMALL(30), - 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/ContentDownloadStateTrackerBinder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentDownloadStateTrackerBinder.kt index caf0131144..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,16 +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, - private val messageColorProvider: MessageColorProvider, - private val errorFormatter: ErrorFormatter) { +class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) { private val updateListeners = mutableMapOf() @@ -39,7 +35,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 +58,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 +118,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/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index b30286163e..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 @@ -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,35 +50,28 @@ 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 val nextDisplayableEvent = params.nextDisplayableEvent + val prevDisplayableEvent = params.prevDisplayableEvent 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() 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() + val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator + 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 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, @@ -90,6 +82,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses SendStateDecoration.NONE } + val messageLayout = messageLayoutFactory.create(params) + return MessageInformationData( eventId = eventId, senderId = event.root.senderId ?: "", @@ -98,8 +92,7 @@ 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(), + messageLayout = messageLayout, orderedReactionList = event.annotations?.reactionsSummary // ?.filter { isSingleEmoji(it.key) } ?.map { @@ -127,6 +120,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/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? = 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/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt index 080b766258..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 @@ -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.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 @@ -98,6 +99,7 @@ abstract class AbsBaseMessageItem : BaseEventItem holder.view.onClick(baseAttributes.itemClickListener) holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener) + (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/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt index 4bd84ae603..9e8f86c26e 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 @@ -25,7 +25,6 @@ import android.widget.RelativeLayout import android.widget.TextView import androidx.annotation.IdRes import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import com.airbnb.epoxy.EpoxyAttribute @@ -75,38 +74,37 @@ abstract class AbsMessageItem : AbsBaseMessageItem override fun bind(holder: H) { super.bind(holder) - if (attributes.informationData.showInformation) { + if (attributes.informationData.messageLayout.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.messageLayout.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.messageLayout.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/BaseEventItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt index 5dfbf5d8f6..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,7 +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 /** * Children must override getViewType() @@ -40,8 +39,18 @@ abstract class BaseEventItem : VectorEpoxyModel @EpoxyAttribute open var leftGuideline: Int = 0 - @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. + val pairingResult = pairingFunction(layout.toLong(), getViewStubId().toLong()) + return (pairingResult - Int.MAX_VALUE).toInt() + } + + abstract fun getViewStubId(): Int + + // Szudzik function + private fun pairingFunction(a: Long, b: Long): Long { + return if (a >= b) a * a + a + b else a + b * b + } @CallSuper override fun bind(holder: H) { 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 5abc9d714c..6db0b0c380 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() { return listOf(attributes.informationData.eventId) } - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID class Holder : BaseHolder(STUB_ID) { val avatarImageView by bind(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() { - 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() { - - @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 getViewType() = STUB_ID - - class Holder : AbsMessageItem.Holder(STUB_ID) { - val messageView by bind(R.id.codeBlockTextView) - val editedView by bind(R.id.codeBlockEditedView) - } - - companion object { - private const val STUB_ID = R.id.messageContentCodeBlockStub - } -} 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..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 @@ -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() { @@ -73,15 +77,19 @@ abstract class MessageFileItem : AbsMessageItem() { } 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) @@ -95,9 +103,10 @@ abstract class MessageFileItem : AbsMessageItem() { contentDownloadStateTrackerBinder.unbind(mxcUrl) } - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { + val mainLayout by bind(R.id.messageFileMainLayout) val progressLayout by bind(R.id.messageFileUploadProgressLayout) val fileLayout by bind(R.id.messageFileLayout) val fileImageView by bind(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 3ae91db97c..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,12 +23,16 @@ 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.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.style.granularRoundedCorners import im.vector.app.features.media.ImageContentRenderer @EpoxyModelClass(layout = R.layout.item_timeline_event_base) @@ -54,7 +58,14 @@ abstract class MessageImageVideoItem : AbsMessageItem(R.id.messageMediaUploadProgressLayout) 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 8258f797f1..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,8 +32,7 @@ data class MessageInformationData( val ageLocalTS: Long?, val avatarUrl: String?, val memberName: CharSequence? = null, - val showInformation: Boolean = true, - val forceShowTimestamp: Boolean = false, + val messageLayout: TimelineMessageLayout, /*List of reactions (emoji,count,isSelected)*/ val orderedReactionList: List? = null, val pollResponseAggregatedSummary: PollResponseData? = null, @@ -41,7 +41,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/item/MessageLocationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt index 607458678e..d60f2b5d05 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() { @@ -34,17 +38,32 @@ abstract class MessageLocationItem : AbsMessageItem( var userId: String? = null @EpoxyAttribute + var mapWidth: Int = 0 + + @EpoxyAttribute + var mapHeight: Int = 0 + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var locationPinProvider: LocationPinProvider? = null override fun bind(holder: Holder) { super.bind(holder) renderSendState(holder.view, null) - val location = locationUrl ?: 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(userId) { pinDrawable -> @@ -54,7 +73,7 @@ abstract class MessageLocationItem : AbsMessageItem( } } - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { val staticMapImageView by bind(R.id.staticMapImageView) 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 e5ac321d90..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,6 +80,7 @@ abstract class MessageTextItem : AbsMessageItem() { safePreviewUrlRetriever.addListener(attributes.informationData.eventId, previewUrlViewUpdater) } holder.previewUrlView.delegate = previewUrlCallback + holder.previewUrlView.renderMessageLayout(attributes.informationData.messageLayout) if (useBigFont) { holder.messageView.textSize = 44F @@ -121,7 +122,7 @@ abstract class MessageTextItem : AbsMessageItem() { previewUrlRetriever?.removeListener(attributes.informationData.eventId, previewUrlViewUpdater) } - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { val messageView by bind(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..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 @@ -16,7 +16,10 @@ 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 @@ -29,6 +32,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() { @@ -80,6 +85,12 @@ abstract class MessageVoiceItem : AbsMessageItem() { } } + 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 { @@ -120,9 +131,10 @@ abstract class MessageVoiceItem : AbsMessageItem() { voiceMessagePlaybackTracker.unTrack(attributes.informationData.eventId) } - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { + val voicePlaybackLayout by bind(R.id.voicePlaybackLayout) val voiceLayout by bind(R.id.voiceLayout) val voicePlaybackControlButton by bind(R.id.voicePlaybackControlButton) val voicePlaybackTime by bind(R.id.voicePlaybackTime) 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 357139648d..3c3510a073 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() { return listOf(attributes.informationData.eventId) } - override fun getViewType() = STUB_ID + override fun getViewStubId() = STUB_ID class Holder : BaseHolder(STUB_ID) { val avatarImageView by bind(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 b660ee9a59..2327a0f2e2 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 @@ -50,6 +50,8 @@ abstract class PollItem : AbsMessageItem() { @EpoxyAttribute lateinit var optionViewStates: List + 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() { - 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 a3d9d3995c..fdde087b44 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 { + buildModernLayout(showInformation) + } + TimelineLayoutSettings.BUBBLE -> { + val shouldBuildBubbleLayout = event.shouldBuildBubbleLayout() + if (shouldBuildBubbleLayout) { + val isFirstFromThisSender = nextDisplayableEvent == null || !nextDisplayableEvent.shouldBuildBubbleLayout() || + nextDisplayableEvent.root.senderId != event.root.senderId || addDaySeparator + + val isLastFromThisSender = prevDisplayableEvent == null || !prevDisplayableEvent.shouldBuildBubbleLayout() || + 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, + cornersRadius = cornersRadius, + isPseudoBubble = messageContent.isPseudoBubble(), + timestampAsOverlay = messageContent.timestampAsOverlay() + ) + } else { + buildModernLayout(showInformation) + } + } + } + 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() + return messageContent?.msgType !in MSG_TYPES_WITHOUT_BUBBLE_LAYOUT + } + return false + } + + private fun buildModernLayout(showInformation: Boolean): TimelineMessageLayout.Default { + return TimelineMessageLayout.Default( + showAvatar = showInformation, + showDisplayName = showInformation, + showTimestamp = showInformation || vectorPreferences.alwaysShowTimeStamps() + ) + } + + 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 + */ + 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 + } + } +} 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..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 @@ -17,17 +17,21 @@ 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.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 import org.matrix.android.sdk.api.session.media.PreviewUrlData /** @@ -37,7 +41,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 @@ -47,7 +51,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 @@ -76,6 +79,22 @@ class PreviewUrlView @JvmOverloads constructor( } } + override fun renderMessageLayout(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) @@ -127,7 +146,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/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..422dfb0dbd --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/MessageBubbleView.kt @@ -0,0 +1,162 @@ +/* + * 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.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.RippleDrawable +import android.util.AttributeSet +import android.view.View +import android.view.ViewOutlineProvider +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 +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 +import im.vector.app.features.home.room.detail.timeline.style.shapeAppearanceModel +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), TimelineMessageLayoutRenderer { + + private var isIncoming: Boolean = false + + private val horizontalStubPadding = DimensionConverter(resources).dpToPx(12) + 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) + context.withStyledAttributes(attrs, R.styleable.MessageBubble) { + isIncoming = getBoolean(R.styleable.MessageBubble_incoming_style, false) + } + } + + override fun onFinishInflate() { + super.onFinishInflate() + views = ViewMessageBubbleBinding.bind(this) + val currentLayoutDirection = LocaleProvider(resources).getLayoutDirectionFromCurrentLocale() + val layoutDirectionToSet = if (isIncoming) { + currentLayoutDirection + } else { + if (currentLayoutDirection == View.LAYOUT_DIRECTION_LTR) { + View.LAYOUT_DIRECTION_RTL + } else { + View.LAYOUT_DIRECTION_LTR + } + } + views.informationBottom.layoutDirection = layoutDirectionToSet + views.messageThreadSummaryContainer.layoutDirection = layoutDirectionToSet + views.bubbleWrapper.layoutDirection = layoutDirectionToSet + 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 renderMessageLayout(messageLayout: TimelineMessageLayout) { + if (messageLayout !is TimelineMessageLayout.Bubble) { + Timber.v("Can't render messageLayout $messageLayout") + return + } + updateDrawables(messageLayout) + ConstraintSet().apply { + clone(views.bubbleView) + clear(R.id.viewStubContainer, ConstraintSet.END) + 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) + } 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.timestampAsOverlay) { + views.messageOverlayView.isVisible = true + (views.messageOverlayView.background as? GradientDrawable)?.cornerRadii = messageLayout.cornersRadius.toFloatArray() + } else { + views.messageOverlayView.isVisible = false + } + if (messageLayout.isPseudoBubble && messageLayout.timestampAsOverlay) { + views.viewStubContainer.root.setPadding(0, 0, 0, 0) + } else { + views.viewStubContainer.root.setPadding(horizontalStubPadding, verticalStubPadding, horizontalStubPadding, verticalStubPadding) + } + if (isIncoming) { + views.messageEndGuideline.updateLayoutParams { + marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end) + } + views.messageStartGuideline.updateLayoutParams { + marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start) + } + } else { + views.messageEndGuideline.updateLayoutParams { + marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start) + } + views.messageStartGuideline.updateLayoutParams { + marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end) + } + } + } + + 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.cornersRadius.shapeAppearanceModel() + 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 + val backgroundColor = ThemeUtils.getColor(context, backgroundColorAttr) + ColorStateList.valueOf(backgroundColor) + } + } + rippleMaskDrawable.shapeAppearanceModel = shapeAppearanceModel + } +} 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 new file mode 100644 index 0000000000..0c42662801 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/view/TimelineMessageLayoutRenderer.kt @@ -0,0 +1,23 @@ +/* + * 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 + +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout + +interface TimelineMessageLayoutRenderer { + fun renderMessageLayout(messageLayout: TimelineMessageLayout) +} 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 36acad8854..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 @@ -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(""".*?""")) { - matchResult -> "$$" + matchResult.groupValues[1] + "$$" + .replace(Regex(""".*?""")) { matchResult -> + "$$" + matchResult.groupValues[1] + "$$" } - .replace(Regex(""".*?""")) { - matchResult -> "\n$$\n" + matchResult.groupValues[1] + "\n$$\n" + .replace(Regex(""".*?""")) { matchResult -> + "\n$$\n" + matchResult.groupValues[1] + "\n$$\n" } } }) @@ -112,12 +114,15 @@ 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(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 { + return listOf("code") + } +} + +/** + * Pre tag are already handled by HtmlPlugin to keep the formatting. + * We are only using it to check for
*
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 { + 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/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/ParagraphHandler.kt b/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt new file mode 100644 index 0000000000..3dd1b4f091 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/html/ParagraphHandler.kt @@ -0,0 +1,42 @@ +/* + * 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 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 + +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) + } + SpannableBuilder.setSpans( + visitor.builder(), + VerticalPaddingSpan(dimensionConverter.dpToPx(4), dimensionConverter.dpToPx(4)), + tag.start(), + tag.end() + ) + } +} 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() 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..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,13 +16,13 @@ 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 +49,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") } 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..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 @@ -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 @@ -42,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 @@ -59,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) { @@ -87,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 } @@ -109,7 +123,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 = RoundedCorners(dimensionConverter.dpToPx(8))) { val size = processSize(data, mode) imageView.updateLayoutParams { width = size.width @@ -120,7 +134,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/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) { 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 f248882211..6a4b5484eb 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" @@ -849,6 +850,15 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_SHOW_EMOJI_KEYBOARD, true) } + /** + * Tells if the timeline messages should be shown in a bubble or not. + * + * @return true to show timeline message in bubble. + */ + fun useMessageBubblesLayout(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_INTERFACE_BUBBLE_KEY, false) + } + /** * Tells if the rage shake is used. * 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/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 @@ + + + + + + + + \ No newline at end of file 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 @@ + + + + \ No newline at end of file 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 e40760e046..b41fda99fb 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 0f487d49fa..bc02728f6e 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" @@ -76,66 +77,16 @@ android:visibility="gone" tools:visibility="visible" /> - - - - - - - - - - - - - - - - - - - + android:addStatesFromChildren="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..0a58a53b88 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_bubble_outgoing_base.xml @@ -0,0 +1,7 @@ + + 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 @@ - - - - - - - - - - - - - - - - - - - - + tools:viewBindingIgnore="true"> - + + + + + + + + + - + + \ No newline at end of file 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 @@ - + android:layout_height="wrap_content"> + + 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" /> + 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" /> - + 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 60fe8dafcd..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 @@ -10,7 +10,6 @@ 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" @@ -25,6 +24,7 @@ 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_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 @@ 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..667db8fee2 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_view_stubs_container.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + 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 @@ - + style="@style/TimelineContentMediaPillStyle"> - + 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 @@ + android:layout_width="32dp" + android:layout_height="32dp" + tools:parentTag="android.widget.FrameLayout"> + tools:progress="40" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_room_detail_toolbar.xml b/vector/src/main/res/layout/view_room_detail_toolbar.xml index 4a534ce867..147c5409f8 100644 --- a/vector/src/main/res/layout/view_room_detail_toolbar.xml +++ b/vector/src/main/res/layout/view_room_detail_toolbar.xml @@ -67,6 +67,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" @@ -85,6 +86,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/view_url_preview.xml b/vector/src/main/res/layout/view_url_preview.xml index 93ea4ea2bb..e9774207ca 100644 --- a/vector/src/main/res/layout/view_url_preview.xml +++ b/vector/src/main/res/layout/view_url_preview.xml @@ -1,77 +1,102 @@ - + + + + android:orientation="vertical" + app:layout_constraintGuide_begin="8dp" /> - + + + - \ No newline at end of file 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 @@ Once enabled you will be able to send your location to any room Render user locations in the timeline + Show Message bubbles + Open camera Send images and videos Upload file diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index db0830bca0..061674c049 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -82,6 +82,11 @@ + +