Merge pull request #4937 from vector-im/feature/fga/message_bubbles

Feature/fga/message bubbles
This commit is contained in:
Benoit Marty 2022-02-03 19:00:38 +01:00 committed by GitHub
commit 4ce1ab2665
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 1563 additions and 517 deletions

1
changelog.d/4937.feature Normal file
View File

@ -0,0 +1 @@
Support message bubbles in timeline.

View File

@ -2,9 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Tint color is provided by the theme -->
<solid android:color="@android:color/black" />
<size
android:width="240dp"
android:height="44dp" />
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"

View File

@ -2,16 +2,14 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="8dp" />
<solid android:color="?vctr_room_active_widgets_banner_bg" />
<shape android:shape="oval">
<solid android:color="?vctr_system" />
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="8dp" />
<shape android:shape="oval">
<solid android:color="@color/vctr_notice_secondary_alpha12" />
</shape>
</clip>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="is_rtl">true</bool>
</resources>

View File

@ -4,6 +4,4 @@
<!-- Created to detect what has to be implemented (especially in the settings) -->
<bool name="false_not_implemented">false</bool>
<bool name="is_rtl">false</bool>
</resources>

View File

@ -137,4 +137,5 @@
<attr name="vctr_presence_indicator_offline" format="color" />
<color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</color>
<color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color>
</resources>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Timeline bubble background colors -->
<attr name="vctr_message_bubble_inbound" format="color" />
<color name="vctr_message_bubble_inbound_light">#E8EDF4</color>
<color name="vctr_message_bubble_inbound_dark">#21262C</color>
<attr name="vctr_message_bubble_outbound" format="color" />
<color name="vctr_message_bubble_outbound_light">#E7F8F3</color>
<color name="vctr_message_bubble_outbound_dark">#133A34</color>
</resources>

View File

@ -15,6 +15,8 @@
<dimen name="item_decoration_left_margin">72dp</dimen>
<dimen name="item_event_message_state_size">16dp</dimen>
<dimen name="item_event_message_media_button_size">32dp</dimen>
<dimen name="chat_avatar_size">40dp</dimen>
<dimen name="member_list_avatar_size">60dp</dimen>
@ -42,6 +44,7 @@
<!-- Preview Url -->
<dimen name="preview_url_view_corner_radius">8dp</dimen>
<dimen name="preview_url_view_image_max_height">160dp</dimen>
<dimen name="menu_item_icon_size">24dp</dimen>
<dimen name="menu_item_size">48dp</dimen>
@ -52,6 +55,12 @@
<dimen name="composer_attachment_size">52dp</dimen>
<dimen name="composer_attachment_margin">1dp</dimen>
<dimen name="chat_bubble_margin_start">28dp</dimen>
<dimen name="chat_bubble_margin_end">62dp</dimen>
<dimen name="chat_bubble_fixed_size">300dp</dimen>
<dimen name="chat_bubble_corner_radius">12dp</dimen>
<!-- Onboarding -->
<item name="ftue_auth_gutter_start_percent" format="float" type="dimen">0.05</item>
<item name="ftue_auth_gutter_end_percent" format="float" type="dimen">0.95</item>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MessageBubble">
<attr name="incoming_style" format="boolean" />
<attr name="show_time_overlay" format="boolean" />
<attr name="is_first" format="boolean" />
<attr name="is_last" format="boolean" />
</declare-styleable>
</resources>

View File

@ -6,6 +6,7 @@
<style name="Widget.Vector.ProgressBar.Horizontal.File">
<item name="android:indeterminateOnly">false</item>
<item name="android:progressDrawable">@drawable/file_progress_bar</item>
<item name="android:progressBackgroundTint">?android:colorBackground</item>
<item name="android:minHeight">10dp</item>
<item name="android:maxHeight">40dp</item>
</style>

View File

@ -4,12 +4,23 @@
<style name="TimelineContentStubBaseParams">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_marginStart">8dp</item>
<item name="android:layout_marginLeft">8dp</item>
<item name="android:layout_marginEnd">8dp</item>
<item name="android:layout_marginRight">8dp</item>
<item name="android:layout_marginBottom">4dp</item>
<item name="android:layout_marginTop">4dp</item>
</style>
<style name="TimelineContentStubContainerParams">
<item name="android:paddingStart">8dp</item>
<item name="android:paddingEnd">8dp</item>
<item name="android:paddingTop">4dp</item>
<item name="android:paddingBottom">4dp</item>
</style>
<style name="TimelineContentMediaPillStyle">
<item name="android:paddingStart">8dp</item>
<item name="android:paddingEnd">8dp</item>
<item name="android:paddingTop">6dp</item>
<item name="android:paddingBottom">6dp</item>
<item name="minHeight">48dp</item>
<item name="android:background">@drawable/bg_media_pill</item>
<item name="android:backgroundTint">?vctr_content_quinary</item>
</style>
</resources>

View File

@ -31,6 +31,8 @@
<item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_dark</item>
<item name="vctr_chat_effect_snow_background">@color/vctr_chat_effect_snow_background_dark</item>
<item name="vctr_toolbar_background">@color/element_system_dark</item>
<item name="vctr_message_bubble_inbound">@color/vctr_message_bubble_inbound_dark</item>
<item name="vctr_message_bubble_outbound">@color/vctr_message_bubble_outbound_dark</item>
<!-- room message colors -->
<item name="vctr_notice_secondary">#61708B</item>

View File

@ -31,6 +31,8 @@
<item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_light</item>
<item name="vctr_chat_effect_snow_background">@color/vctr_chat_effect_snow_background_light</item>
<item name="vctr_toolbar_background">@color/element_background_light</item>
<item name="vctr_message_bubble_inbound">@color/vctr_message_bubble_inbound_light</item>
<item name="vctr_message_bubble_outbound">@color/vctr_message_bubble_outbound_light</item>
<!-- room message colors -->
<item name="vctr_notice_secondary">#61708B</item>

View File

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

View File

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

View File

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

View File

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

View File

@ -27,5 +27,7 @@ internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData(
siteName = siteName,
title = title,
description = description,
mxcUrl = mxcUrl
mxcUrl = mxcUrl,
imageWidth = imageWidth,
imageHeight = imageHeight
)

View File

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

View File

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

View File

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

View File

@ -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<Event>()
return when (messageContent) {
val messageItem = when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
@ -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)

View File

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

View File

@ -17,14 +17,22 @@
package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.style.TimelineLayoutSettings
import im.vector.app.features.home.room.detail.timeline.style.TimelineLayoutSettingsProvider
import javax.inject.Inject
class AvatarSizeProvider @Inject constructor(private val dimensionConverter: DimensionConverter) {
class AvatarSizeProvider @Inject constructor(private val dimensionConverter: DimensionConverter,
private val layoutSettingsProvider: TimelineLayoutSettingsProvider) {
private val avatarStyle = AvatarStyle.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)
}
}
}

View File

@ -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<String, ContentDownloadUpdater>()
@ -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)
}
}

View File

@ -27,7 +27,7 @@ import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData
import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory
import org.matrix.android.sdk.api.crypto.VerificationState
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited
import org.matrix.android.sdk.api.session.room.timeline.isEdition
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import javax.inject.Inject
@ -51,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
)

View File

@ -16,13 +16,17 @@
package im.vector.app.features.home.room.detail.timeline.helper
import android.content.res.Resources
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.scopes.ActivityScoped
import im.vector.app.R
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
import kotlin.math.roundToInt
@ActivityScoped
class TimelineMediaSizeProvider @Inject constructor() {
class TimelineMediaSizeProvider @Inject constructor(private val resources: Resources,
private val vectorPreferences: VectorPreferences) {
var recyclerView: RecyclerView? = null
private var cachedSize: Pair<Int, Int>? = null
@ -41,9 +45,14 @@ class TimelineMediaSizeProvider @Inject constructor() {
maxImageWidth = (width * 0.7f).roundToInt()
maxImageHeight = (height * 0.5f).roundToInt()
} else {
maxImageWidth = (width * 0.5f).roundToInt()
maxImageWidth = (width * 0.7f).roundToInt()
maxImageHeight = (height * 0.7f).roundToInt()
}
return Pair(maxImageWidth, maxImageHeight)
return if (vectorPreferences.useMessageBubblesLayout()) {
val bubbleMaxImageWidth = maxImageWidth.coerceAtMost(resources.getDimensionPixelSize(R.dimen.chat_bubble_fixed_size))
Pair(bubbleMaxImageWidth, maxImageHeight)
} else {
Pair(maxImageWidth, maxImageHeight)
}
}
}

View File

@ -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<H : AbsBaseMessageItem.Holder> : BaseEventItem
holder.view.onClick(baseAttributes.itemClickListener)
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
(holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout)
}
override fun unbind(holder: H) {

View File

@ -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<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
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

View File

@ -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<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
@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) {

View File

@ -50,7 +50,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
@EpoxyAttribute
lateinit var attributes: Attributes
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
override fun bind(holder: Holder) {
super.bind(holder)

View File

@ -46,7 +46,7 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
return listOf(attributes.informationData.eventId)
}
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
class Holder : BaseHolder(STUB_ID) {
val avatarImageView by bind<ImageView>(R.id.itemDefaultAvatarView)

View File

@ -29,7 +29,7 @@ import im.vector.app.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
@EpoxyAttribute
override lateinit var attributes: Attributes

View File

@ -51,7 +51,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var movementMethod: MovementMethod? = null
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
override fun bind(holder: Holder) {
super.bind(holder)

View File

@ -1,57 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.timeline.item
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import me.saket.bettermovementmethod.BetterLinkMovementMethod
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageBlockCodeItem : AbsMessageItem<MessageBlockCodeItem.Holder>() {
@EpoxyAttribute
var message: EpoxyCharSequence? = null
@EpoxyAttribute
var editedSpan: EpoxyCharSequence? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.messageView.text = message?.charSequence
renderSendState(holder.messageView, holder.messageView)
holder.messageView.onClick(attributes.itemClickListener)
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
holder.editedView.movementMethod = BetterLinkMovementMethod.getInstance()
holder.editedView.setTextOrHide(editedSpan?.charSequence)
}
override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val messageView by bind<TextView>(R.id.codeBlockTextView)
val editedView by bind<TextView>(R.id.codeBlockEditedView)
}
companion object {
private const val STUB_ID = R.id.messageContentCodeBlockStub
}
}

View File

@ -16,6 +16,8 @@
package im.vector.app.features.home.room.detail.timeline.item
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Paint
import android.view.ViewGroup
import android.widget.ImageView
@ -29,6 +31,8 @@ import im.vector.app.R
import im.vector.app.core.epoxy.onClick
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.themes.ThemeUtils
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
@ -73,15 +77,19 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
} else {
if (izDownloaded) {
holder.fileImageView.setImageResource(iconRes)
holder.fileDownloadProgress.progress = 100
holder.fileDownloadProgress.progress = 0
} else {
contentDownloadStateTrackerBinder.bind(mxcUrl, holder)
holder.fileImageView.setImageResource(R.drawable.ic_download)
holder.fileDownloadProgress.progress = 0
}
}
// holder.view.setOnClickListener(clickListener)
val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) {
Color.TRANSPARENT
} else {
ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary)
}
holder.mainLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
holder.filenameView.onClick(attributes.itemClickListener)
holder.filenameView.setOnLongClickListener(attributes.itemLongClickListener)
holder.fileImageWrapper.onClick(attributes.itemClickListener)
@ -95,9 +103,10 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
contentDownloadStateTrackerBinder.unbind(mxcUrl)
}
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val mainLayout by bind<ViewGroup>(R.id.messageFileMainLayout)
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
val fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
val fileImageView by bind<ImageView>(R.id.messageFileIconView)

View File

@ -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<MessageImageVideoItem.Hold
override fun bind(holder: Holder) {
super.bind(holder)
imageContentRenderer.render(mediaData, mode, holder.imageView)
val messageLayout = baseAttributes.informationData.messageLayout
val dimensionConverter = DimensionConverter(holder.view.resources)
val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
messageLayout.cornersRadius.granularRoundedCorners()
} else {
RoundedCorners(dimensionConverter.dpToPx(8))
}
imageContentRenderer.render(mediaData, mode, holder.imageView, imageCornerTransformation)
if (!attributes.informationData.sendState.hasFailed()) {
contentUploadStateTrackerBinder.bind(
attributes.informationData.eventId,
@ -81,7 +92,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
super.unbind(holder)
}
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)

View File

@ -17,6 +17,7 @@
package im.vector.app.features.home.room.detail.timeline.item
import android.os.Parcelable
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.crypto.VerificationState
import org.matrix.android.sdk.api.session.room.send.SendState
@ -31,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<ReactionInfoData>? = 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

View File

@ -17,12 +17,16 @@
package im.vector.app.features.home.room.detail.timeline.item
import android.widget.ImageView
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import im.vector.app.R
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() {
@ -34,17 +38,32 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
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<MessageLocationItem.Holder>(
}
}
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val staticMapImageView by bind<ImageView>(R.id.staticMapImageView)

View File

@ -80,6 +80,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
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<MessageTextItem.Holder>() {
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<AppCompatTextView>(R.id.messageTextView)

View File

@ -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<MessageVoiceItem.Holder>() {
@ -80,6 +85,12 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
}
}
val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) {
Color.TRANSPARENT
} else {
ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary)
}
holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener {
@ -120,9 +131,10 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
voiceMessagePlaybackTracker.unTrack(attributes.informationData.eventId)
}
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val voicePlaybackLayout by bind<View>(R.id.voicePlaybackLayout)
val voiceLayout by bind<ViewGroup>(R.id.voiceLayout)
val voicePlaybackControlButton by bind<ImageButton>(R.id.voicePlaybackControlButton)
val voicePlaybackTime by bind<TextView>(R.id.voicePlaybackTime)

View File

@ -64,7 +64,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
return listOf(attributes.informationData.eventId)
}
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
class Holder : BaseHolder(STUB_ID) {
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)

View File

@ -50,6 +50,8 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
@EpoxyAttribute
lateinit var optionViewStates: List<PollOptionViewState>
override fun getViewStubId() = STUB_ID
override fun bind(holder: Holder) {
super.bind(holder)
val relatedEventId = eventId ?: return

View File

@ -22,7 +22,7 @@ import im.vector.app.R
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class RedactedMessageItem : AbsMessageItem<RedactedMessageItem.Holder>() {
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
override fun shouldShowReactionAtBottom() = false

View File

@ -40,7 +40,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem<StatusTileTimelineIte
@EpoxyAttribute
lateinit var attributes: Attributes
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
@SuppressLint("SetTextI18n")
override fun bind(holder: Holder) {

View File

@ -51,7 +51,7 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
@EpoxyAttribute
var callback: TimelineEventController.Callback? = null
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
@SuppressLint("SetTextI18n")
override fun bind(holder: Holder) {

View File

@ -41,7 +41,7 @@ abstract class WidgetTileTimelineItem : AbsBaseMessageItem<WidgetTileTimelineIte
@EpoxyAttribute
lateinit var attributes: Attributes
override fun getViewType() = STUB_ID
override fun getViewStubId() = STUB_ID
@SuppressLint("SetTextI18n")
override fun bind(holder: Holder) {

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.timeline.style
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
fun TimelineMessageLayout.Bubble.CornersRadius.granularRoundedCorners(): GranularRoundedCorners {
return GranularRoundedCorners(topStartRadius, topEndRadius, bottomEndRadius, bottomStartRadius)
}
fun TimelineMessageLayout.Bubble.CornersRadius.shapeAppearanceModel(): ShapeAppearanceModel {
return ShapeAppearanceModel().toBuilder()
.setTopRightCorner(topEndRadius.cornerFamily(), topEndRadius)
.setBottomRightCorner(bottomEndRadius.cornerFamily(), bottomEndRadius)
.setTopLeftCorner(topStartRadius.cornerFamily(), topStartRadius)
.setBottomLeftCorner(bottomStartRadius.cornerFamily(), bottomStartRadius)
.build()
}
private fun Float.cornerFamily(): Int {
return if (this == 0F) CornerFamily.CUT else CornerFamily.ROUNDED
}

View File

@ -0,0 +1,22 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.timeline.style
enum class TimelineLayoutSettings {
MODERN,
BUBBLE
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.timeline.style
import im.vector.app.features.settings.VectorPreferences
import javax.inject.Inject
class TimelineLayoutSettingsProvider @Inject constructor(private val vectorPreferences: VectorPreferences) {
fun getLayoutSettings(): TimelineLayoutSettings {
return if (vectorPreferences.useMessageBubblesLayout()) {
TimelineLayoutSettings.BUBBLE
} else {
TimelineLayoutSettings.MODERN
}
}
}

View File

@ -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.home.room.detail.timeline.style
import android.os.Parcelable
import im.vector.app.R
import kotlinx.parcelize.Parcelize
sealed interface TimelineMessageLayout : Parcelable {
val layoutRes: Int
val showAvatar: Boolean
val showDisplayName: Boolean
val showTimestamp: Boolean
@Parcelize
data class Default(override val showAvatar: Boolean,
override val showDisplayName: Boolean,
override val showTimestamp: Boolean,
// Keep defaultLayout generated on epoxy items
override val layoutRes: Int = 0) : TimelineMessageLayout
@Parcelize
data class Bubble(
override val showAvatar: Boolean,
override val showDisplayName: Boolean,
override val showTimestamp: Boolean = true,
val isIncoming: Boolean,
val isPseudoBubble: Boolean,
val cornersRadius: CornersRadius,
val timestampAsOverlay: Boolean,
override val layoutRes: Int = if (isIncoming) {
R.layout.item_timeline_event_bubble_incoming_base
} else {
R.layout.item_timeline_event_bubble_outgoing_base
}
) : TimelineMessageLayout {
@Parcelize
data class CornersRadius(
val topStartRadius: Float,
val topEndRadius: Float,
val bottomStartRadius: Float,
val bottomEndRadius: Float
) : Parcelable
}
}

View File

@ -0,0 +1,197 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.timeline.style
import android.content.res.Resources
import im.vector.app.R
import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.resources.LocaleProvider
import im.vector.app.core.resources.isRTL
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.session.room.timeline.isEdition
import javax.inject.Inject
class TimelineMessageLayoutFactory @Inject constructor(private val session: Session,
private val layoutSettingsProvider: TimelineLayoutSettingsProvider,
private val localeProvider: LocaleProvider,
private val resources: Resources,
private val vectorPreferences: VectorPreferences) {
companion object {
// Can be rendered in bubbles, other types will fallback to default
private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf(
EventType.MESSAGE,
EventType.POLL_START,
EventType.ENCRYPTED,
EventType.STICKER
)
// Can't be rendered in bubbles, so get back to default layout
private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf(
MessageType.MSGTYPE_VERIFICATION_REQUEST
)
// Use the bubble layout but without borders
private val MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT = setOf(
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_STICKER_LOCAL,
MessageType.MSGTYPE_EMOTE
)
private val MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY = setOf(
MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_VIDEO
)
}
private val cornerRadius: Float by lazy {
resources.getDimensionPixelSize(R.dimen.chat_bubble_corner_radius).toFloat()
}
private val isRTL: Boolean by lazy {
localeProvider.isRTL()
}
fun create(params: TimelineItemFactoryParams): TimelineMessageLayout {
val event = params.event
val nextDisplayableEvent = params.nextDisplayableEvent
val prevDisplayableEvent = params.prevDisplayableEvent
val isSentByMe = event.root.senderId == session.myUserId
val date = event.root.localDateTime()
val nextDate = nextDisplayableEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
?: false
val showInformation = addDaySeparator ||
event.senderInfo.avatarUrl != nextDisplayableEvent?.senderInfo?.avatarUrl ||
event.senderInfo.disambiguatedDisplayName != nextDisplayableEvent?.senderInfo?.disambiguatedDisplayName ||
nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) ||
isNextMessageReceivedMoreThanOneHourAgo ||
isTileTypeMessage(nextDisplayableEvent) ||
nextDisplayableEvent.isEdition()
val messageLayout = when (layoutSettingsProvider.getLayoutSettings()) {
TimelineLayoutSettings.MODERN -> {
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
}
}
}

View File

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

View File

@ -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<LayoutParams> {
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end)
}
views.messageStartGuideline.updateLayoutParams<LayoutParams> {
marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start)
}
} else {
views.messageEndGuideline.updateLayoutParams<LayoutParams> {
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start)
}
views.messageStartGuideline.updateLayoutParams<LayoutParams> {
marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end)
}
}
}
private fun 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
}
}

View File

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

View File

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

View File

@ -17,9 +17,11 @@
package im.vector.app.features.html
import android.content.Context
import android.content.res.Resources
import android.text.Spannable
import androidx.core.text.toSpannable
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.settings.VectorPreferences
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
@ -53,11 +55,11 @@ class EventHtmlRenderer @Inject constructor(
.usePlugin(object : AbstractMarkwonPlugin() { // Markwon expects maths to be in a specific format: https://noties.io/Markwon/docs/v4/ext-latex
override fun processMarkdown(markdown: String): String {
return markdown
.replace(Regex("""<span\s+data-mx-maths="([^"]*)">.*?</span>""")) {
matchResult -> "$$" + matchResult.groupValues[1] + "$$"
.replace(Regex("""<span\s+data-mx-maths="([^"]*)">.*?</span>""")) { matchResult ->
"$$" + matchResult.groupValues[1] + "$$"
}
.replace(Regex("""<div\s+data-mx-maths="([^"]*)">.*?</div>""")) {
matchResult -> "\n$$\n" + matchResult.groupValues[1] + "\n$$\n"
.replace(Regex("""<div\s+data-mx-maths="([^"]*)">.*?</div>""")) { matchResult ->
"\n$$\n" + matchResult.groupValues[1] + "\n$$\n"
}
}
})
@ -112,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))
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.html
import io.noties.markwon.MarkwonVisitor
import io.noties.markwon.SpannableBuilder
import io.noties.markwon.html.HtmlTag
import io.noties.markwon.html.MarkwonHtmlRenderer
import io.noties.markwon.html.TagHandler
class CodeTagHandler : TagHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
SpannableBuilder.setSpans(
visitor.builder(),
HtmlCodeSpan(visitor.configuration().theme(), false),
tag.start(),
tag.end()
)
}
override fun supportedTags(): List<String> {
return listOf("code")
}
}
/**
* Pre tag are already handled by HtmlPlugin to keep the formatting.
* We are only using it to check for <pre><code>*</code></pre> tags.
*/
class CodePreTagHandler : TagHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val htmlCodeSpan = visitor.builder()
.getSpans(tag.start(), tag.end())
.firstOrNull {
it.what is HtmlCodeSpan
}
if (htmlCodeSpan != null) {
(htmlCodeSpan.what as HtmlCodeSpan).isBlock = true
}
}
override fun supportedTags(): List<String> {
return listOf("pre")
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
*

View File

@ -53,7 +53,6 @@ class RoomWidgetPermissionBottomSheet :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
}

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/transparent"/>
<stroke
android:width="2dp"
android:color="?android:colorBackground"/>
</shape>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="linear"
android:angle="270"
android:startColor="#00000000"
android:endColor="#33000000"/>
</shape>

View File

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

View File

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

View File

@ -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" />
<FrameLayout
<include
android:id="@+id/viewStubContainer"
android:layout_width="match_parent"
layout="@layout/item_timeline_event_view_stubs_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="@id/messageMemberNameView"
android:layout_marginEnd="8dp"
android:layout_toStartOf="@id/messageSendStateImageView"
android:layout_toEndOf="@id/messageStartGuideline"
android:addStatesFromChildren="true">
<ViewStub
android:id="@+id/messageContentTextStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_text_message_stub"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentCodeBlockStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_code_block_stub"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentMediaStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:inflatedId="@+id/messageContentMedia"
android:layout="@layout/item_timeline_event_media_message_stub" />
<ViewStub
android:id="@+id/messageContentFileStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_file_stub" />
<ViewStub
android:id="@+id/messageContentRedactedStub"
style="@style/TimelineContentStubBaseParams"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_redacted_stub" />
<ViewStub
android:id="@+id/messageContentVoiceStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_voice_stub"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentPollStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_poll" />
<ViewStub
android:id="@+id/messageContentLocationStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_location_stub" />
</FrameLayout>
android:addStatesFromChildren="true" />
<im.vector.app.core.ui.views.SendStateImageView
android:id="@+id/messageSendStateImageView"

View File

@ -23,6 +23,7 @@
<FrameLayout
android:id="@+id/viewStubContainer"
style="@style/TimelineContentStubContainerParams"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"

View File

@ -31,6 +31,7 @@
<FrameLayout
android:id="@+id/viewStubContainer"
style="@style/TimelineContentStubContainerParams"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<im.vector.app.features.home.room.detail.timeline.view.MessageBubbleView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
app:incoming_style="true" />

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<im.vector.app.features.home.room.detail.timeline.view.MessageBubbleView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
app:incoming_style="false" />

View File

@ -19,9 +19,8 @@
<TextView
android:id="@+id/itemDefaultTextView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"

View File

@ -1,80 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/messageFileLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:orientation="vertical"
tools:viewBindingIgnore="true">
<im.vector.app.core.ui.views.ShieldImageView
android:id="@+id/messageFilee2eIcon"
android:layout_width="14dp"
android:layout_height="14dp"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<!-- the media type -->
<RelativeLayout
android:id="@+id/messageFileImageView"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
app:layout_constraintStart_toEndOf="@id/messageFilee2eIcon"
app:layout_constraintTop_toTopOf="parent">
<include layout="@layout/view_file_icon" />
</RelativeLayout>
<!-- <ImageView-->
<!-- android:id="@+id/messageFileImageView"-->
<!-- android:layout_width="@dimen/chat_avatar_size"-->
<!-- android:layout_height="@dimen/chat_avatar_size"-->
<!-- android:layout_marginStart="4dp"-->
<!-- app:layout_constraintStart_toEndOf="@id/messageFilee2eIcon"-->
<!-- app:layout_constraintTop_toTopOf="parent"-->
<!-- tools:src="@drawable/filetype_attachment" />-->
<!-- the media -->
<TextView
android:id="@+id/messageFilenameView"
style="@style/Widget.Vector.TextView.Body"
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/messageFileMainLayout"
style="@style/TimelineContentMediaPillStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="32dp"
android:autoLink="none"
android:gravity="center_vertical"
android:minHeight="@dimen/chat_avatar_size"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/messageFileImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="A filename here" />
tools:viewBindingIgnore="true">
<androidx.constraintlayout.widget.Barrier
android:id="@+id/horizontalBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="messageFileImageView,messageFilenameView" />
<FrameLayout
android:id="@+id/messageFileImageView"
android:layout_width="32dp"
android:layout_height="32dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent">
<include layout="@layout/view_file_icon" />
</FrameLayout>
<!-- the file name-->
<TextView
android:id="@+id/messageFilenameView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:autoLink="none"
android:maxLines="1"
android:ellipsize="end"
android:gravity="center_vertical"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/messageFileImageView"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="A filename here" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/messageFileUploadProgressLayout"
layout="@layout/media_upload_download_progress_layout"
android:layout_width="0dp"
android:layout_height="46dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/horizontalBarrier"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -1,16 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="8dp">
android:layout_height="wrap_content">
<!-- Size will be overrode -->
<ImageView
android:id="@+id/staticMapImageView"
android:layout_width="match_parent"
android:layout_width="300dp"
android:layout_height="200dp"
android:contentDescription="@string/a11y_static_map_image" />
android:contentDescription="@string/a11y_static_map_image"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/backgrounds/scenic" />
<ImageView
android:id="@+id/staticMapPinImageView"
@ -19,6 +24,10 @@
android:layout_gravity="center"
android:layout_marginBottom="28dp"
android:importantForAccessibility="no"
android:src="@drawable/bg_map_user_pin" />
android:src="@drawable/bg_map_user_pin"
app:layout_constraintBottom_toBottomOf="@id/staticMapImageView"
app:layout_constraintEnd_toEndOf="@id/staticMapImageView"
app:layout_constraintStart_toStartOf="@id/staticMapImageView"
app:layout_constraintTop_toTopOf="@id/staticMapImageView" />
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

@ -19,9 +19,8 @@
<TextView
android:id="@+id/itemNoticeTextView"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"

View File

@ -2,17 +2,19 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:minWidth="@dimen/chat_bubble_fixed_size"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/questionTextView"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

View File

@ -17,11 +17,10 @@
<im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView
android:id="@+id/messageUrlPreview"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"
android:foreground="?attr/selectableItemBackground"
android:visibility="gone"
tools:visibility="visible" />

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/TimelineContentStubContainerParams"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:addStatesFromChildren="true">
<ViewStub
android:id="@+id/messageContentTextStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_text_message_stub"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentMediaStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/messageContentMedia"
android:layout="@layout/item_timeline_event_media_message_stub" />
<ViewStub
android:id="@+id/messageContentFileStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_file_stub"
tools:visibility="gone" />
<ViewStub
android:id="@+id/messageContentRedactedStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_redacted_stub" />
<ViewStub
android:id="@+id/messageContentVoiceStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_voice_stub"
tools:visibility="gone" />
<ViewStub
android:id="@+id/messageContentPollStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_poll" />
<ViewStub
android:id="@+id/messageContentLocationStub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/item_timeline_event_location_stub" />
</FrameLayout>

View File

@ -1,31 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/voiceLayout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="@dimen/chat_bubble_fixed_size"
tools:viewBindingIgnore="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/voicePlaybackLayout"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_voice_playback"
android:backgroundTint="?vctr_content_quinary"
android:minHeight="48dp"
android:paddingStart="8dp"
android:paddingTop="6dp"
android:paddingEnd="8dp"
android:paddingBottom="6dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
style="@style/TimelineContentMediaPillStyle">
<ImageButton
android:id="@+id/voicePlaybackControlButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_width="@dimen/item_event_message_media_button_size"
android:layout_height="@dimen/item_event_message_media_button_size"
android:background="@drawable/bg_voice_play_pause_button"
android:backgroundTint="?android:colorBackground"
android:contentDescription="@string/a11y_play_voice_message"
@ -65,16 +58,12 @@
<include
android:id="@+id/messageFileUploadProgressLayout"
layout="@layout/media_upload_download_progress_layout"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="46dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/voicePlaybackLayout"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

View File

@ -2,22 +2,22 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="50dp"
android:layout_height="50dp"
tools:parentTag="android.widget.RelativeLayout">
android:layout_width="32dp"
android:layout_height="32dp"
tools:parentTag="android.widget.FrameLayout">
<ProgressBar
android:id="@+id/messageFileProgressbar"
style="@style/Widget.Vector.ProgressBar.Horizontal.File"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:progress="40" />
tools:progress="40" />
<ImageView
android:id="@+id/messageFileIconView"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_centerInParent="true"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="center"
android:contentDescription="@string/attachment_type_file"
android:src="@drawable/ic_download"
app:tint="?vctr_notice_secondary"

View File

@ -0,0 +1,228 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="android.widget.RelativeLayout">
<im.vector.app.core.platform.CheckableView
android:id="@+id/messageSelectedBackground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignBottom="@id/informationBottom"
android:layout_alignParentTop="true"
android:background="@drawable/highlighted_message_background" />
<ImageView
android:id="@+id/messageAvatarImageView"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:background="@drawable/bg_avatar_border"
android:contentDescription="@string/avatar"
android:elevation="2dp"
android:padding="2dp"
tools:src="@sample/user_round_avatars" />
<TextView
android:id="@+id/messageMemberNameView"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginStart="12dp"
android:layout_marginEnd="4dp"
android:layout_toEndOf="@id/messageStartGuideline"
android:ellipsize="end"
android:maxLines="1"
android:textAlignment="viewStart"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
tools:text="@sample/users.json/data/displayName" />
<View
android:id="@+id/messageStartGuideline"
android:layout_width="0dp"
android:layout_height="0dp"
tools:layout_marginStart="52dp" />
<View
android:id="@+id/messageEndGuideline"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_alignParentEnd="true"
android:layout_marginEnd="64dp" />
<Space
android:id="@+id/decorationSpace"
android:layout_width="4dp"
android:layout_height="8dp"
android:layout_toEndOf="@id/messageStartGuideline" />
<im.vector.app.core.ui.views.ShieldImageView
android:id="@+id/messageE2EDecoration"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_alignTop="@id/bubbleWrapper"
android:layout_alignEnd="@id/decorationSpace"
android:layout_marginTop="7dp"
android:elevation="2dp"
android:visibility="gone"
tools:visibility="visible" />
<FrameLayout
android:id="@+id/bubbleWrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/messageMemberNameView"
android:layout_toStartOf="@id/messageEndGuideline"
android:layout_toEndOf="@id/messageStartGuideline"
android:addStatesFromChildren="true"
android:clipChildren="false"
android:clipToPadding="false">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/bubbleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:addStatesFromChildren="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<include
android:id="@+id/viewStubContainer"
layout="@layout/item_timeline_event_view_stubs_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:addStatesFromChildren="true"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="@dimen/chat_bubble_fixed_size" />
<View
android:id="@+id/messageOverlayView"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/overlay_bubble_media"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/viewStubContainer"
app:layout_constraintEnd_toEndOf="@id/viewStubContainer"
app:layout_constraintStart_toStartOf="@id/viewStubContainer"
app:layout_constraintTop_toTopOf="@id/viewStubContainer"
tools:visibility="visible" />
<TextView
android:id="@+id/messageTimeView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:textColor="?vctr_content_tertiary"
android:textSize="10sp"
app:layout_constraintBottom_toBottomOf="@id/viewStubContainer"
app:layout_constraintEnd_toEndOf="parent"
tools:text="@tools:sample/date/hhmm" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
<im.vector.app.core.ui.views.SendStateImageView
android:id="@+id/messageSendStateImageView"
android:layout_width="@dimen/item_event_message_state_size"
android:layout_height="@dimen/item_event_message_state_size"
android:layout_alignBottom="@id/bubbleWrapper"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:contentDescription="@string/event_status_a11y_sending"
android:src="@drawable/ic_sending_message"
android:visibility="invisible"
tools:tint="?vctr_content_tertiary"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/eventSendingIndicator"
android:layout_width="@dimen/item_event_message_state_size"
android:layout_height="@dimen/item_event_message_state_size"
android:layout_alignBottom="@id/bubbleWrapper"
android:layout_alignParentEnd="true"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp"
android:indeterminateTint="?vctr_content_secondary"
android:visibility="gone"
app:tint="?vctr_content_tertiary"
tools:ignore="MissingPrefix"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/informationBottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/bubbleWrapper"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:layout_toStartOf="@id/messageEndGuideline"
android:layout_toEndOf="@id/messageStartGuideline"
android:addStatesFromChildren="true"
android:orientation="vertical">
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/reactionsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
app:dividerDrawable="@drawable/reaction_divider"
app:flexWrap="wrap"
app:showDivider="middle"
tools:background="#F0E0F0"
tools:layout_height="40dp">
<!-- ReactionButtons will be added here in the code -->
<!--im.vector.app.features.reactions.widget.ReactionButton
android:layout_width="wrap_content"
android:layout_height="wrap_content" /-->
</com.google.android.flexbox.FlexboxLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/messageThreadSummaryContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/informationBottom"
android:layout_toStartOf="@id/messageEndGuideline"
android:layout_toEndOf="@id/messageStartGuideline"
android:contentDescription="@string/room_threads_filter">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/messageThreadSummaryConstraintLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:background="@drawable/rounded_rect_shape_8"
android:contentDescription="@string/room_threads_filter"
android:layoutDirection="locale"
android:maxWidth="496dp"
android:minWidth="144dp"
android:paddingStart="13dp"
android:paddingTop="8dp"
android:paddingEnd="13dp"
android:paddingBottom="8dp"
android:visibility="gone"
tools:visibility="visible">
<include layout="@layout/view_thread_room_summary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
</merge>

View File

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

View File

@ -1,77 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/informationUrlPreviewContainer"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="com.google.android.material.card.MaterialCardView">
<LinearLayout
android:layout_width="wrap_content"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="208dp"
android:orientation="vertical">
<!--Image dimensions will be overrode by ImageContentRenderer -->
<ImageView
android:id="@+id/url_preview_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintHeight_max="208dp"
android:scaleType="centerCrop"
android:importantForAccessibility="no"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:src="@tools:sample/backgrounds/scenic" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/url_preview_start_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
android:maxHeight="200dp"
android:scaleType="fitXY"
tools:src="@tools:sample/backgrounds/scenic" />
android:orientation="vertical"
app:layout_constraintGuide_begin="8dp" />
<TextView
android:id="@+id/url_preview_site"
style="@style/Widget.Vector.TextView.Caption"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="6dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
app:layout_constraintEnd_toStartOf="@id/url_preview_close"
android:textColor="?vctr_content_secondary"
app:layout_constraintStart_toStartOf="@id/url_preview_start_guideline"
app:layout_constraintTop_toBottomOf="@id/url_preview_image"
app:layout_goneMarginTop="12dp"
tools:text="BBC News" />
<TextView
android:id="@+id/url_preview_title"
style="@style/Widget.Vector.TextView.Body.Medium"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="@dimen/layout_touch_size"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@id/url_preview_close"
app:layout_constraintStart_toStartOf="@id/url_preview_start_guideline"
app:layout_constraintTop_toBottomOf="@id/url_preview_site"
app:layout_goneMarginTop="12dp"
tools:text="Jo Malone denounces her former brand's John Boyega decision" />
<TextView
android:id="@+id/url_preview_description"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:ellipsize="end"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/url_preview_start_guideline"
app:layout_constraintTop_toBottomOf="@id/url_preview_title"
tools:text="The British perfumer says removing actor John Boyega from his own advert was “utterly despicable”." />
</LinearLayout>
<ImageView
android:id="@+id/url_preview_close"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_gravity="top|end"
android:contentDescription="@string/action_close"
android:scaleType="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/ic_close_with_circular_bg"
tools:ignore="MissingPrefix" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/url_preview_close"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_gravity="top|end"
android:contentDescription="@string/action_close"
android:scaleType="center"
android:src="@drawable/ic_close_with_circular_bg"
tools:ignore="MissingPrefix" />
</merge>

View File

@ -160,7 +160,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/bg_voice_playback"
android:background="@drawable/bg_media_pill"
android:backgroundTint="?vctr_content_quinary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@ -3768,6 +3768,8 @@
<string name="settings_enable_location_sharing_summary">Once enabled you will be able to send your location to any room</string>
<string name="labs_render_locations_in_timeline">Render user locations in the timeline</string>
<string name="message_bubbles">Show Message bubbles</string>
<string name="tooltip_attachment_photo">Open camera</string>
<string name="tooltip_attachment_gallery">Send images and videos</string>
<string name="tooltip_attachment_file">Upload file</string>

View File

@ -82,6 +82,11 @@
<im.vector.app.core.preference.VectorPreferenceCategory android:title="@string/settings_category_timeline">
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="false"
android:key="SETTINGS_INTERFACE_BUBBLE_KEY"
android:title="@string/message_bubbles" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="true"
android:key="SETTINGS_SHOW_URL_PREVIEW_KEY"