Merge pull request #4937 from vector-im/feature/fga/message_bubbles
Feature/fga/message bubbles
This commit is contained in:
commit
4ce1ab2665
|
@ -0,0 +1 @@
|
|||
Support message bubbles in timeline.
|
|
@ -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"
|
|
@ -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>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<bool name="is_rtl">true</bool>
|
||||
|
||||
</resources>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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?
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -27,5 +27,7 @@ internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData(
|
|||
siteName = siteName,
|
||||
title = title,
|
||||
description = description,
|
||||
mxcUrl = mxcUrl
|
||||
mxcUrl = mxcUrl,
|
||||
imageWidth = imageWidth,
|
||||
imageHeight = imageHeight
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -382,7 +382,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
(0 until modelCache.size).forEach { position ->
|
||||
val event = currentSnapshot[position]
|
||||
val nextEvent = currentSnapshot.nextOrNull(position)
|
||||
// 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,
|
||||
|
@ -390,12 +399,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
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 timelineEventsGroup = timelineEventsGroups.getOrNull(event)
|
||||
val params = TimelineItemFactoryParams(
|
||||
event = event,
|
||||
prevEvent = prevEvent,
|
||||
prevDisplayableEvent = prevDisplayableEvent,
|
||||
nextEvent = nextEvent,
|
||||
nextDisplayableEvent = nextDisplayableEvent,
|
||||
partialState = partialState,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.avatarImageView.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.avatarImageView.visibility = View.GONE
|
||||
if (attributes.informationData.forceShowTimestamp) {
|
||||
holder.memberNameView.isInvisible = true
|
||||
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.memberNameView.isVisible = false
|
||||
holder.timeView.isVisible = false
|
||||
}
|
||||
holder.avatarImageView.setOnLongClickListener(null)
|
||||
holder.memberNameView.setOnLongClickListener(null)
|
||||
}
|
||||
|
||||
// Render send state indicator
|
||||
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
|
||||
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
visitChildren(visitor, renderer, tag.asBlock)
|
||||
val replyText = visitor.builder().removeFromEnd(tag.end())
|
||||
visitor.builder().append("\n\n").append(replyText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -53,7 +53,6 @@ class RoomWidgetPermissionBottomSheet :
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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" />
|
|
@ -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" />
|
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/messageFileMainLayout"
|
||||
style="@style/TimelineContentMediaPillStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:viewBindingIgnore="true">
|
||||
|
||||
<!-- the media type -->
|
||||
<RelativeLayout
|
||||
<FrameLayout
|
||||
android:id="@+id/messageFileImageView"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="4dp"
|
||||
app:layout_constraintStart_toEndOf="@id/messageFilee2eIcon"
|
||||
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" />
|
||||
</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" />-->
|
||||
</FrameLayout>
|
||||
|
||||
<!-- the media -->
|
||||
<!-- 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="4dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:autoLink="none"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
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"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:text="A filename here" />
|
||||
|
||||
<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" />
|
||||
</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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -1,69 +1,89 @@
|
|||
<?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"
|
||||
|
@ -71,7 +91,12 @@
|
|||
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>
|
||||
|
||||
|
||||
</merge>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue