Merge pull request #4937 from vector-im/feature/fga/message_bubbles
Feature/fga/message bubbles
This commit is contained in:
commit
4ce1ab2665
1
changelog.d/4937.feature
Normal file
1
changelog.d/4937.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Support message bubbles in timeline.
|
@ -2,9 +2,6 @@
|
|||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<!-- Tint color is provided by the theme -->
|
<!-- Tint color is provided by the theme -->
|
||||||
<solid android:color="@android:color/black" />
|
<solid android:color="@android:color/black" />
|
||||||
<size
|
|
||||||
android:width="240dp"
|
|
||||||
android:height="44dp" />
|
|
||||||
<corners
|
<corners
|
||||||
android:bottomLeftRadius="12dp"
|
android:bottomLeftRadius="12dp"
|
||||||
android:bottomRightRadius="12dp"
|
android:bottomRightRadius="12dp"
|
@ -2,16 +2,14 @@
|
|||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<item android:id="@android:id/background">
|
<item android:id="@android:id/background">
|
||||||
<shape>
|
<shape android:shape="oval">
|
||||||
<corners android:radius="8dp" />
|
<solid android:color="?vctr_system" />
|
||||||
<solid android:color="?vctr_room_active_widgets_banner_bg" />
|
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item android:id="@android:id/progress">
|
<item android:id="@android:id/progress">
|
||||||
<clip>
|
<clip>
|
||||||
<shape>
|
<shape android:shape="oval">
|
||||||
<corners android:radius="8dp" />
|
|
||||||
<solid android:color="@color/vctr_notice_secondary_alpha12" />
|
<solid android:color="@color/vctr_notice_secondary_alpha12" />
|
||||||
</shape>
|
</shape>
|
||||||
</clip>
|
</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) -->
|
<!-- Created to detect what has to be implemented (especially in the settings) -->
|
||||||
<bool name="false_not_implemented">false</bool>
|
<bool name="false_not_implemented">false</bool>
|
||||||
|
|
||||||
<bool name="is_rtl">false</bool>
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -137,4 +137,5 @@
|
|||||||
<attr name="vctr_presence_indicator_offline" format="color" />
|
<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_light">@color/palette_gray_100</color>
|
||||||
<color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color>
|
<color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color>
|
||||||
|
|
||||||
</resources>
|
</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_decoration_left_margin">72dp</dimen>
|
||||||
<dimen name="item_event_message_state_size">16dp</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="chat_avatar_size">40dp</dimen>
|
||||||
<dimen name="member_list_avatar_size">60dp</dimen>
|
<dimen name="member_list_avatar_size">60dp</dimen>
|
||||||
|
|
||||||
@ -42,6 +44,7 @@
|
|||||||
|
|
||||||
<!-- Preview Url -->
|
<!-- Preview Url -->
|
||||||
<dimen name="preview_url_view_corner_radius">8dp</dimen>
|
<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_icon_size">24dp</dimen>
|
||||||
<dimen name="menu_item_size">48dp</dimen>
|
<dimen name="menu_item_size">48dp</dimen>
|
||||||
@ -52,6 +55,12 @@
|
|||||||
<dimen name="composer_attachment_size">52dp</dimen>
|
<dimen name="composer_attachment_size">52dp</dimen>
|
||||||
<dimen name="composer_attachment_margin">1dp</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 -->
|
<!-- Onboarding -->
|
||||||
<item name="ftue_auth_gutter_start_percent" format="float" type="dimen">0.05</item>
|
<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>
|
<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">
|
<style name="Widget.Vector.ProgressBar.Horizontal.File">
|
||||||
<item name="android:indeterminateOnly">false</item>
|
<item name="android:indeterminateOnly">false</item>
|
||||||
<item name="android:progressDrawable">@drawable/file_progress_bar</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:minHeight">10dp</item>
|
||||||
<item name="android:maxHeight">40dp</item>
|
<item name="android:maxHeight">40dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
@ -4,12 +4,23 @@
|
|||||||
<style name="TimelineContentStubBaseParams">
|
<style name="TimelineContentStubBaseParams">
|
||||||
<item name="android:layout_width">match_parent</item>
|
<item name="android:layout_width">match_parent</item>
|
||||||
<item name="android:layout_height">wrap_content</item>
|
<item name="android:layout_height">wrap_content</item>
|
||||||
<item name="android:layout_marginStart">8dp</item>
|
</style>
|
||||||
<item name="android:layout_marginLeft">8dp</item>
|
|
||||||
<item name="android:layout_marginEnd">8dp</item>
|
<style name="TimelineContentStubContainerParams">
|
||||||
<item name="android:layout_marginRight">8dp</item>
|
<item name="android:paddingStart">8dp</item>
|
||||||
<item name="android:layout_marginBottom">4dp</item>
|
<item name="android:paddingEnd">8dp</item>
|
||||||
<item name="android:layout_marginTop">4dp</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>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
@ -31,6 +31,8 @@
|
|||||||
<item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_dark</item>
|
<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_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_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 -->
|
<!-- room message colors -->
|
||||||
<item name="vctr_notice_secondary">#61708B</item>
|
<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_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_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_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 -->
|
<!-- room message colors -->
|
||||||
<item name="vctr_notice_secondary">#61708B</item>
|
<item name="vctr_notice_secondary">#61708B</item>
|
||||||
|
@ -47,5 +47,9 @@ data class PreviewUrlData(
|
|||||||
// Value of field "og:description"
|
// Value of field "og:description"
|
||||||
val description: String?,
|
val description: String?,
|
||||||
// Value of field "og:image"
|
// 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 {
|
) : RealmMigration {
|
||||||
|
|
||||||
companion object {
|
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 <= 20) migrateTo21(realm)
|
||||||
if (oldVersion <= 21) migrateTo22(realm)
|
if (oldVersion <= 21) migrateTo22(realm)
|
||||||
if (oldVersion <= 22) migrateTo23(realm)
|
if (oldVersion <= 22) migrateTo23(realm)
|
||||||
|
if (oldVersion <= 23) migrateTo24(realm)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo1(realm: DynamicRealm) {
|
private fun migrateTo1(realm: DynamicRealm) {
|
||||||
@ -479,4 +480,13 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
}
|
}
|
||||||
?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity)
|
?.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 title: String? = null,
|
||||||
var description: String? = null,
|
var description: String? = null,
|
||||||
var mxcUrl: String? = null,
|
var mxcUrl: String? = null,
|
||||||
|
var imageWidth: Int? = null,
|
||||||
|
var imageHeight: Int? = null,
|
||||||
var lastUpdatedTimestamp: Long = 0L
|
var lastUpdatedTimestamp: Long = 0L
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
|
@ -48,8 +48,8 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData {
|
override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData {
|
||||||
return when (params.cacheStrategy) {
|
return when (params.cacheStrategy) {
|
||||||
CacheStrategy.NoCache -> doRequest(params.url, params.timestamp)
|
CacheStrategy.NoCache -> doRequest(params.url, params.timestamp)
|
||||||
is CacheStrategy.TtlCache -> doRequestWithCache(
|
is CacheStrategy.TtlCache -> doRequestWithCache(
|
||||||
params.url,
|
params.url,
|
||||||
params.timestamp,
|
params.timestamp,
|
||||||
params.cacheStrategy.validityDurationInMillis,
|
params.cacheStrategy.validityDurationInMillis,
|
||||||
@ -77,7 +77,9 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
|
|||||||
siteName = (get("og:site_name") as? String)?.unescapeHtml(),
|
siteName = (get("og:site_name") as? String)?.unescapeHtml(),
|
||||||
title = (get("og:title") as? String)?.unescapeHtml(),
|
title = (get("og:title") as? String)?.unescapeHtml(),
|
||||||
description = (get("og:description") 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.title = data.title
|
||||||
previewUrlCacheEntity.description = data.description
|
previewUrlCacheEntity.description = data.description
|
||||||
previewUrlCacheEntity.mxcUrl = data.mxcUrl
|
previewUrlCacheEntity.mxcUrl = data.mxcUrl
|
||||||
|
previewUrlCacheEntity.imageHeight = data.imageHeight
|
||||||
|
previewUrlCacheEntity.imageWidth = data.imageWidth
|
||||||
previewUrlCacheEntity.lastUpdatedTimestamp = Date().time
|
previewUrlCacheEntity.lastUpdatedTimestamp = Date().time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,5 +27,7 @@ internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData(
|
|||||||
siteName = siteName,
|
siteName = siteName,
|
||||||
title = title,
|
title = title,
|
||||||
description = description,
|
description = description,
|
||||||
mxcUrl = mxcUrl
|
mxcUrl = mxcUrl,
|
||||||
|
imageWidth = imageWidth,
|
||||||
|
imageHeight = imageHeight
|
||||||
)
|
)
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
package im.vector.app.core.resources
|
package im.vector.app.core.resources
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.view.View
|
||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
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.isEnglishSpeaking() = current().language.startsWith("en")
|
||||||
|
|
||||||
|
fun LocaleProvider.getLayoutDirectionFromCurrentLocale() = TextUtils.getLayoutDirectionFromLocale(current())
|
||||||
|
|
||||||
|
fun LocaleProvider.isRTL() = getLayoutDirectionFromCurrentLocale() == View.LAYOUT_DIRECTION_RTL
|
||||||
|
@ -382,20 +382,28 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
(0 until modelCache.size).forEach { position ->
|
(0 until modelCache.size).forEach { position ->
|
||||||
val event = currentSnapshot[position]
|
val event = currentSnapshot[position]
|
||||||
val nextEvent = currentSnapshot.nextOrNull(position)
|
val nextEvent = currentSnapshot.nextOrNull(position)
|
||||||
val prevEvent = currentSnapshot.prevOrNull(position)
|
|
||||||
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
|
|
||||||
timelineEventVisibilityHelper.shouldShowEvent(
|
|
||||||
timelineEvent = it,
|
|
||||||
highlightedEventId = partialState.highlightedEventId,
|
|
||||||
isFromThreadTimeline = partialState.isFromThreadTimeline(),
|
|
||||||
rootThreadEventId = partialState.rootThreadEventId)
|
|
||||||
}
|
|
||||||
// Should be build if not cached or if model should be refreshed
|
// Should be build if not cached or if model should be refreshed
|
||||||
if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState) == false) {
|
if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState) == false) {
|
||||||
|
val prevEvent = currentSnapshot.prevOrNull(position)
|
||||||
|
val prevDisplayableEvent = currentSnapshot.subList(0, position).lastOrNull {
|
||||||
|
timelineEventVisibilityHelper.shouldShowEvent(
|
||||||
|
timelineEvent = it,
|
||||||
|
highlightedEventId = partialState.highlightedEventId,
|
||||||
|
isFromThreadTimeline = partialState.isFromThreadTimeline(),
|
||||||
|
rootThreadEventId = partialState.rootThreadEventId)
|
||||||
|
}
|
||||||
|
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
|
||||||
|
timelineEventVisibilityHelper.shouldShowEvent(
|
||||||
|
timelineEvent = it,
|
||||||
|
highlightedEventId = partialState.highlightedEventId,
|
||||||
|
isFromThreadTimeline = partialState.isFromThreadTimeline(),
|
||||||
|
rootThreadEventId = partialState.rootThreadEventId)
|
||||||
|
}
|
||||||
val timelineEventsGroup = timelineEventsGroups.getOrNull(event)
|
val timelineEventsGroup = timelineEventsGroups.getOrNull(event)
|
||||||
val params = TimelineItemFactoryParams(
|
val params = TimelineItemFactoryParams(
|
||||||
event = event,
|
event = event,
|
||||||
prevEvent = prevEvent,
|
prevEvent = prevEvent,
|
||||||
|
prevDisplayableEvent = prevDisplayableEvent,
|
||||||
nextEvent = nextEvent,
|
nextEvent = nextEvent,
|
||||||
nextDisplayableEvent = nextDisplayableEvent,
|
nextDisplayableEvent = nextDisplayableEvent,
|
||||||
partialState = partialState,
|
partialState = partialState,
|
||||||
|
@ -113,6 +113,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
|||||||
callback = params.callback,
|
callback = params.callback,
|
||||||
threadDetails = threadDetails)
|
threadDetails = threadDetails)
|
||||||
return MessageTextItem_()
|
return MessageTextItem_()
|
||||||
|
.layout(informationData.messageLayout.layoutRes)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.highlighted(params.isHighlighted)
|
.highlighted(params.isHighlighted)
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.factory
|
package im.vector.app.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
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.TimelineMediaSizeProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
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.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.MessageFileItem_
|
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
|
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.item.VerificationRequestItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
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.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.EventHtmlRenderer
|
||||||
import im.vector.app.features.html.PillsPostProcessor
|
import im.vector.app.features.html.PillsPostProcessor
|
||||||
import im.vector.app.features.html.SpanUtils
|
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.app.features.settings.VectorPreferences
|
||||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||||
import me.gujun.android.span.span
|
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.MatrixUrls.isMxcUrl
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
@ -134,7 +129,6 @@ class MessageItemFactory @Inject constructor(
|
|||||||
private val locationPinProvider: LocationPinProvider,
|
private val locationPinProvider: LocationPinProvider,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val urlMapProvider: UrlMapProvider,
|
private val urlMapProvider: UrlMapProvider,
|
||||||
private val resources: Resources
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// TODO inject this properly?
|
// TODO inject this properly?
|
||||||
@ -181,7 +175,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
|
|
||||||
// val all = event.root.toContent()
|
// val all = event.root.toContent()
|
||||||
// val ev = all.toModel<Event>()
|
// val ev = all.toModel<Event>()
|
||||||
return when (messageContent) {
|
val messageItem = when (messageContent) {
|
||||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
|
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
|
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
@ -206,13 +200,16 @@ class MessageItemFactory @Inject constructor(
|
|||||||
}
|
}
|
||||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
|
return messageItem?.apply {
|
||||||
|
layout(informationData.messageLayout.layoutRes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildLocationItem(locationContent: MessageLocationContent,
|
private fun buildLocationItem(locationContent: MessageLocationContent,
|
||||||
informationData: MessageInformationData,
|
informationData: MessageInformationData,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
attributes: AbsMessageItem.Attributes): MessageLocationItem? {
|
attributes: AbsMessageItem.Attributes): MessageLocationItem? {
|
||||||
val width = resources.displayMetrics.widthPixels - dimensionConverter.dpToPx(60)
|
val width = timelineMediaSizeProvider.getMaxSize().first
|
||||||
val height = dimensionConverter.dpToPx(200)
|
val height = dimensionConverter.dpToPx(200)
|
||||||
|
|
||||||
val locationUrl = locationContent.toLocationData()?.let {
|
val locationUrl = locationContent.toLocationData()?.let {
|
||||||
@ -224,6 +221,8 @@ class MessageItemFactory @Inject constructor(
|
|||||||
return MessageLocationItem_()
|
return MessageLocationItem_()
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.locationUrl(locationUrl)
|
.locationUrl(locationUrl)
|
||||||
|
.mapWidth(width)
|
||||||
|
.mapHeight(height)
|
||||||
.userId(userId)
|
.userId(userId)
|
||||||
.locationPinProvider(locationPinProvider)
|
.locationPinProvider(locationPinProvider)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
@ -526,46 +525,22 @@ class MessageItemFactory @Inject constructor(
|
|||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
|
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
|
||||||
val isFormatted = messageContent.matrixFormattedBody.isNullOrBlank().not()
|
val matrixFormattedBody = messageContent.matrixFormattedBody
|
||||||
return if (isFormatted) {
|
return if (matrixFormattedBody != null) {
|
||||||
// First detect if the message contains some code block(s) or inline code
|
buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
|
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildFormattedTextItem(messageContent: MessageTextContent,
|
private fun buildFormattedTextItem(matrixFormattedBody: String,
|
||||||
informationData: MessageInformationData,
|
informationData: MessageInformationData,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||||
val compressed = htmlCompressor.compress(messageContent.formattedBody!!)
|
val compressed = htmlCompressor.compress(matrixFormattedBody)
|
||||||
val formattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor)
|
val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned
|
||||||
return buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
|
return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildMessageTextItem(body: CharSequence,
|
private fun buildMessageTextItem(body: CharSequence,
|
||||||
@ -598,24 +573,6 @@ class MessageItemFactory @Inject constructor(
|
|||||||
.movementMethod(createLinkMovementMethod(callback))
|
.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,
|
private fun annotateWithEdited(linkifiedBody: CharSequence,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
informationData: MessageInformationData): Spannable {
|
informationData: MessageInformationData): Spannable {
|
||||||
@ -721,6 +678,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
private fun buildRedactedItem(attributes: AbsMessageItem.Attributes,
|
private fun buildRedactedItem(attributes: AbsMessageItem.Attributes,
|
||||||
highlight: Boolean): RedactedMessageItem? {
|
highlight: Boolean): RedactedMessageItem? {
|
||||||
return RedactedMessageItem_()
|
return RedactedMessageItem_()
|
||||||
|
.layout(attributes.informationData.messageLayout.layoutRes)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
|
@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
|||||||
data class TimelineItemFactoryParams(
|
data class TimelineItemFactoryParams(
|
||||||
val event: TimelineEvent,
|
val event: TimelineEvent,
|
||||||
val prevEvent: TimelineEvent? = null,
|
val prevEvent: TimelineEvent? = null,
|
||||||
|
val prevDisplayableEvent: TimelineEvent? = null,
|
||||||
val nextEvent: TimelineEvent? = null,
|
val nextEvent: TimelineEvent? = null,
|
||||||
val nextDisplayableEvent: TimelineEvent? = null,
|
val nextDisplayableEvent: TimelineEvent? = null,
|
||||||
val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(),
|
val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(),
|
||||||
|
@ -17,14 +17,22 @@
|
|||||||
package im.vector.app.features.home.room.detail.timeline.helper
|
package im.vector.app.features.home.room.detail.timeline.helper
|
||||||
|
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.style.TimelineLayoutSettings
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.style.TimelineLayoutSettingsProvider
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AvatarSizeProvider @Inject constructor(private val dimensionConverter: DimensionConverter) {
|
class AvatarSizeProvider @Inject constructor(private val dimensionConverter: DimensionConverter,
|
||||||
|
private val layoutSettingsProvider: TimelineLayoutSettingsProvider) {
|
||||||
|
|
||||||
private val avatarStyle = AvatarStyle.SMALL
|
private val avatarStyle by lazy {
|
||||||
|
when (layoutSettingsProvider.getLayoutSettings()) {
|
||||||
|
TimelineLayoutSettings.MODERN -> AvatarStyle.SMALL
|
||||||
|
TimelineLayoutSettings.BUBBLE -> AvatarStyle.BUBBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val leftGuideline: Int by lazy {
|
val leftGuideline: Int by lazy {
|
||||||
dimensionConverter.dpToPx(avatarStyle.avatarSizeDP + 8)
|
dimensionConverter.dpToPx(avatarStyle.avatarSizeDP + avatarStyle.marginDP)
|
||||||
}
|
}
|
||||||
|
|
||||||
val avatarSize: Int by lazy {
|
val avatarSize: Int by lazy {
|
||||||
@ -33,11 +41,12 @@ class AvatarSizeProvider @Inject constructor(private val dimensionConverter: Dim
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
enum class AvatarStyle(val avatarSizeDP: Int) {
|
enum class AvatarStyle(val avatarSizeDP: Int, val marginDP: Int) {
|
||||||
BIG(50),
|
BIG(50, 8),
|
||||||
MEDIUM(40),
|
MEDIUM(40, 8),
|
||||||
SMALL(30),
|
SMALL(30, 8),
|
||||||
NONE(0)
|
BUBBLE(28, 4),
|
||||||
|
NONE(0, 8)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,16 +22,12 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|||||||
import dagger.hilt.android.scopes.ActivityScoped
|
import dagger.hilt.android.scopes.ActivityScoped
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
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 im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
|
||||||
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
|
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ActivityScoped
|
@ActivityScoped
|
||||||
class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) {
|
||||||
private val messageColorProvider: MessageColorProvider,
|
|
||||||
private val errorFormatter: ErrorFormatter) {
|
|
||||||
|
|
||||||
private val updateListeners = mutableMapOf<String, ContentDownloadUpdater>()
|
private val updateListeners = mutableMapOf<String, ContentDownloadUpdater>()
|
||||||
|
|
||||||
@ -39,7 +35,7 @@ class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSe
|
|||||||
holder: MessageFileItem.Holder) {
|
holder: MessageFileItem.Holder) {
|
||||||
activeSessionHolder.getSafeActiveSession()?.also { session ->
|
activeSessionHolder.getSafeActiveSession()?.also { session ->
|
||||||
val downloadStateTracker = session.contentDownloadProgressTracker()
|
val downloadStateTracker = session.contentDownloadProgressTracker()
|
||||||
val updateListener = ContentDownloadUpdater(holder, messageColorProvider, errorFormatter)
|
val updateListener = ContentDownloadUpdater(holder)
|
||||||
updateListeners[mxcUrl] = updateListener
|
updateListeners[mxcUrl] = updateListener
|
||||||
downloadStateTracker.track(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 class ContentDownloadUpdater(private val holder: MessageFileItem.Holder) : ContentDownloadStateTracker.UpdateListener {
|
||||||
private val messageColorProvider: MessageColorProvider,
|
|
||||||
private val errorFormatter: ErrorFormatter) : ContentDownloadStateTracker.UpdateListener {
|
|
||||||
|
|
||||||
override fun onDownloadStateUpdate(state: ContentDownloadStateTracker.State) {
|
override fun onDownloadStateUpdate(state: ContentDownloadStateTracker.State) {
|
||||||
when (state) {
|
when (state) {
|
||||||
@ -124,7 +118,7 @@ private class ContentDownloadUpdater(private val holder: MessageFileItem.Holder,
|
|||||||
private fun handleSuccess() {
|
private fun handleSuccess() {
|
||||||
stop()
|
stop()
|
||||||
holder.fileDownloadProgress.isIndeterminate = false
|
holder.fileDownloadProgress.isIndeterminate = false
|
||||||
holder.fileDownloadProgress.progress = 100
|
holder.fileDownloadProgress.progress = 0
|
||||||
holder.fileImageView.setImageResource(R.drawable.ic_paperclip)
|
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.ReactionInfoData
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
|
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
|
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory
|
||||||
import org.matrix.android.sdk.api.crypto.VerificationState
|
import org.matrix.android.sdk.api.crypto.VerificationState
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState
|
|||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited
|
import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.isEdition
|
|
||||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -51,35 +50,28 @@ import javax.inject.Inject
|
|||||||
*/
|
*/
|
||||||
class MessageInformationDataFactory @Inject constructor(private val session: Session,
|
class MessageInformationDataFactory @Inject constructor(private val session: Session,
|
||||||
private val dateFormatter: VectorDateFormatter,
|
private val dateFormatter: VectorDateFormatter,
|
||||||
private val visibilityHelper: TimelineEventVisibilityHelper,
|
private val messageLayoutFactory: TimelineMessageLayoutFactory) {
|
||||||
private val vectorPreferences: VectorPreferences) {
|
|
||||||
|
|
||||||
fun create(params: TimelineItemFactoryParams): MessageInformationData {
|
fun create(params: TimelineItemFactoryParams): MessageInformationData {
|
||||||
val event = params.event
|
val event = params.event
|
||||||
val nextDisplayableEvent = params.nextDisplayableEvent
|
val nextDisplayableEvent = params.nextDisplayableEvent
|
||||||
|
val prevDisplayableEvent = params.prevDisplayableEvent
|
||||||
val eventId = event.eventId
|
val eventId = event.eventId
|
||||||
|
val isSentByMe = event.root.senderId == session.myUserId
|
||||||
|
val roomSummary = params.partialState.roomSummary
|
||||||
|
|
||||||
val date = event.root.localDateTime()
|
val date = event.root.localDateTime()
|
||||||
val nextDate = nextDisplayableEvent?.root?.localDateTime()
|
val nextDate = nextDisplayableEvent?.root?.localDateTime()
|
||||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||||
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
|
||||||
?: false
|
|
||||||
|
|
||||||
val showInformation =
|
val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator
|
||||||
addDaySeparator ||
|
val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId ||
|
||||||
event.senderInfo.avatarUrl != nextDisplayableEvent?.senderInfo?.avatarUrl ||
|
prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate()
|
||||||
event.senderInfo.disambiguatedDisplayName != nextDisplayableEvent?.senderInfo?.disambiguatedDisplayName ||
|
|
||||||
nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) ||
|
|
||||||
isNextMessageReceivedMoreThanOneHourAgo ||
|
|
||||||
isTileTypeMessage(nextDisplayableEvent) ||
|
|
||||||
nextDisplayableEvent.isEdition()
|
|
||||||
|
|
||||||
val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
|
val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
|
||||||
val roomSummary = params.partialState.roomSummary
|
|
||||||
val e2eDecoration = getE2EDecoration(roomSummary, event)
|
val e2eDecoration = getE2EDecoration(roomSummary, event)
|
||||||
|
|
||||||
// SendState Decoration
|
// SendState Decoration
|
||||||
val isSentByMe = event.root.senderId == session.myUserId
|
|
||||||
val sendStateDecoration = if (isSentByMe) {
|
val sendStateDecoration = if (isSentByMe) {
|
||||||
getSendStateDecoration(
|
getSendStateDecoration(
|
||||||
event = event,
|
event = event,
|
||||||
@ -90,6 +82,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
SendStateDecoration.NONE
|
SendStateDecoration.NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val messageLayout = messageLayoutFactory.create(params)
|
||||||
|
|
||||||
return MessageInformationData(
|
return MessageInformationData(
|
||||||
eventId = eventId,
|
eventId = eventId,
|
||||||
senderId = event.root.senderId ?: "",
|
senderId = event.root.senderId ?: "",
|
||||||
@ -98,8 +92,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
ageLocalTS = event.root.ageLocalTs,
|
ageLocalTS = event.root.ageLocalTs,
|
||||||
avatarUrl = event.senderInfo.avatarUrl,
|
avatarUrl = event.senderInfo.avatarUrl,
|
||||||
memberName = event.senderInfo.disambiguatedDisplayName,
|
memberName = event.senderInfo.disambiguatedDisplayName,
|
||||||
showInformation = showInformation,
|
messageLayout = messageLayout,
|
||||||
forceShowTimestamp = vectorPreferences.alwaysShowTimeStamps(),
|
|
||||||
orderedReactionList = event.annotations?.reactionsSummary
|
orderedReactionList = event.annotations?.reactionsSummary
|
||||||
// ?.filter { isSingleEmoji(it.key) }
|
// ?.filter { isSingleEmoji(it.key) }
|
||||||
?.map {
|
?.map {
|
||||||
@ -127,6 +120,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||||||
ReferencesInfoData(verificationState)
|
ReferencesInfoData(verificationState)
|
||||||
},
|
},
|
||||||
sentByMe = isSentByMe,
|
sentByMe = isSentByMe,
|
||||||
|
isFirstFromThisSender = isFirstFromThisSender,
|
||||||
|
isLastFromThisSender = isLastFromThisSender,
|
||||||
e2eDecoration = e2eDecoration,
|
e2eDecoration = e2eDecoration,
|
||||||
sendStateDecoration = sendStateDecoration
|
sendStateDecoration = sendStateDecoration
|
||||||
)
|
)
|
||||||
|
@ -16,13 +16,17 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.helper
|
package im.vector.app.features.home.room.detail.timeline.helper
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import dagger.hilt.android.scopes.ActivityScoped
|
import dagger.hilt.android.scopes.ActivityScoped
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@ActivityScoped
|
@ActivityScoped
|
||||||
class TimelineMediaSizeProvider @Inject constructor() {
|
class TimelineMediaSizeProvider @Inject constructor(private val resources: Resources,
|
||||||
|
private val vectorPreferences: VectorPreferences) {
|
||||||
|
|
||||||
var recyclerView: RecyclerView? = null
|
var recyclerView: RecyclerView? = null
|
||||||
private var cachedSize: Pair<Int, Int>? = null
|
private var cachedSize: Pair<Int, Int>? = null
|
||||||
@ -41,9 +45,14 @@ class TimelineMediaSizeProvider @Inject constructor() {
|
|||||||
maxImageWidth = (width * 0.7f).roundToInt()
|
maxImageWidth = (width * 0.7f).roundToInt()
|
||||||
maxImageHeight = (height * 0.5f).roundToInt()
|
maxImageHeight = (height * 0.5f).roundToInt()
|
||||||
} else {
|
} else {
|
||||||
maxImageWidth = (width * 0.5f).roundToInt()
|
maxImageWidth = (width * 0.7f).roundToInt()
|
||||||
maxImageHeight = (height * 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.AvatarRenderer
|
||||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
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.TimelineEventController
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
|
||||||
import im.vector.app.features.reactions.widget.ReactionButton
|
import im.vector.app.features.reactions.widget.ReactionButton
|
||||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
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.onClick(baseAttributes.itemClickListener)
|
||||||
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
|
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
|
||||||
|
(holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: H) {
|
override fun unbind(holder: H) {
|
||||||
|
@ -25,7 +25,6 @@ import android.widget.RelativeLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.view.isInvisible
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
@ -75,38 +74,37 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||||||
|
|
||||||
override fun bind(holder: H) {
|
override fun bind(holder: H) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
if (attributes.informationData.showInformation) {
|
if (attributes.informationData.messageLayout.showAvatar) {
|
||||||
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
|
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
|
||||||
height = attributes.avatarSize
|
height = attributes.avatarSize
|
||||||
width = attributes.avatarSize
|
width = attributes.avatarSize
|
||||||
}
|
}
|
||||||
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)
|
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
|
||||||
holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener)
|
holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
|
holder.avatarImageView.isVisible = true
|
||||||
|
holder.avatarImageView.onClick(_avatarClickListener)
|
||||||
} else {
|
} else {
|
||||||
holder.avatarImageView.setOnClickListener(null)
|
holder.avatarImageView.setOnClickListener(null)
|
||||||
holder.memberNameView.setOnClickListener(null)
|
|
||||||
holder.avatarImageView.visibility = View.GONE
|
|
||||||
if (attributes.informationData.forceShowTimestamp) {
|
|
||||||
holder.memberNameView.isInvisible = true
|
|
||||||
holder.timeView.isVisible = true
|
|
||||||
holder.timeView.text = attributes.informationData.time
|
|
||||||
} else {
|
|
||||||
holder.memberNameView.isVisible = false
|
|
||||||
holder.timeView.isVisible = false
|
|
||||||
}
|
|
||||||
holder.avatarImageView.setOnLongClickListener(null)
|
holder.avatarImageView.setOnLongClickListener(null)
|
||||||
holder.memberNameView.setOnLongClickListener(null)
|
holder.avatarImageView.isVisible = false
|
||||||
|
}
|
||||||
|
if (attributes.informationData.messageLayout.showDisplayName) {
|
||||||
|
holder.memberNameView.isVisible = true
|
||||||
|
holder.memberNameView.text = attributes.informationData.memberName
|
||||||
|
holder.memberNameView.setTextColor(attributes.getMemberNameColor())
|
||||||
|
holder.memberNameView.onClick(_memberNameClickListener)
|
||||||
|
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
|
} else {
|
||||||
|
holder.memberNameView.setOnClickListener(null)
|
||||||
|
holder.memberNameView.setOnLongClickListener(null)
|
||||||
|
holder.memberNameView.isVisible = false
|
||||||
|
}
|
||||||
|
if (attributes.informationData.messageLayout.showTimestamp) {
|
||||||
|
holder.timeView.isVisible = true
|
||||||
|
holder.timeView.text = attributes.informationData.time
|
||||||
|
} else {
|
||||||
|
holder.timeView.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render send state indicator
|
// Render send state indicator
|
||||||
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
|
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
|
||||||
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
|
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.VectorEpoxyHolder
|
||||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||||
import im.vector.app.core.platform.CheckableView
|
import im.vector.app.core.platform.CheckableView
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Children must override getViewType()
|
* Children must override getViewType()
|
||||||
@ -40,8 +39,18 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
open var leftGuideline: Int = 0
|
open var leftGuideline: Int = 0
|
||||||
|
|
||||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
final override fun getViewType(): Int {
|
||||||
lateinit var dimensionConverter: DimensionConverter
|
// 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
|
@CallSuper
|
||||||
override fun bind(holder: H) {
|
override fun bind(holder: H) {
|
||||||
|
@ -50,7 +50,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
|||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
lateinit var attributes: Attributes
|
lateinit var attributes: Attributes
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
|
@ -46,7 +46,7 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
|
|||||||
return listOf(attributes.informationData.eventId)
|
return listOf(attributes.informationData.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
class Holder : BaseHolder(STUB_ID) {
|
class Holder : BaseHolder(STUB_ID) {
|
||||||
val avatarImageView by bind<ImageView>(R.id.itemDefaultAvatarView)
|
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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||||
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
|
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
override lateinit var attributes: Attributes
|
override lateinit var attributes: Attributes
|
||||||
|
@ -51,7 +51,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
|||||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||||
var movementMethod: MovementMethod? = null
|
var movementMethod: MovementMethod? = null
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(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
|
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.graphics.Paint
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@ -29,6 +31,8 @@ import im.vector.app.R
|
|||||||
import im.vector.app.core.epoxy.onClick
|
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.ContentDownloadStateTrackerBinder
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||||
@ -73,15 +77,19 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
|||||||
} else {
|
} else {
|
||||||
if (izDownloaded) {
|
if (izDownloaded) {
|
||||||
holder.fileImageView.setImageResource(iconRes)
|
holder.fileImageView.setImageResource(iconRes)
|
||||||
holder.fileDownloadProgress.progress = 100
|
holder.fileDownloadProgress.progress = 0
|
||||||
} else {
|
} else {
|
||||||
contentDownloadStateTrackerBinder.bind(mxcUrl, holder)
|
contentDownloadStateTrackerBinder.bind(mxcUrl, holder)
|
||||||
holder.fileImageView.setImageResource(R.drawable.ic_download)
|
holder.fileImageView.setImageResource(R.drawable.ic_download)
|
||||||
holder.fileDownloadProgress.progress = 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// holder.view.setOnClickListener(clickListener)
|
// 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.onClick(attributes.itemClickListener)
|
||||||
holder.filenameView.setOnLongClickListener(attributes.itemLongClickListener)
|
holder.filenameView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
holder.fileImageWrapper.onClick(attributes.itemClickListener)
|
holder.fileImageWrapper.onClick(attributes.itemClickListener)
|
||||||
@ -95,9 +103,10 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
|||||||
contentDownloadStateTrackerBinder.unbind(mxcUrl)
|
contentDownloadStateTrackerBinder.unbind(mxcUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder(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 progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
|
||||||
val fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
|
val fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
|
||||||
val fileImageView by bind<ImageView>(R.id.messageFileIconView)
|
val fileImageView by bind<ImageView>(R.id.messageFileIconView)
|
||||||
|
@ -23,12 +23,16 @@ import androidx.core.view.ViewCompat
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.epoxy.ClickListener
|
import im.vector.app.core.epoxy.ClickListener
|
||||||
import im.vector.app.core.epoxy.onClick
|
import im.vector.app.core.epoxy.onClick
|
||||||
import im.vector.app.core.files.LocalFilesHelper
|
import im.vector.app.core.files.LocalFilesHelper
|
||||||
import im.vector.app.core.glide.GlideApp
|
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.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
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
@ -54,7 +58,14 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(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()) {
|
if (!attributes.informationData.sendState.hasFailed()) {
|
||||||
contentUploadStateTrackerBinder.bind(
|
contentUploadStateTrackerBinder.bind(
|
||||||
attributes.informationData.eventId,
|
attributes.informationData.eventId,
|
||||||
@ -81,7 +92,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||||||
super.unbind(holder)
|
super.unbind(holder)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||||
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
|
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package im.vector.app.features.home.room.detail.timeline.item
|
package im.vector.app.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.matrix.android.sdk.api.crypto.VerificationState
|
import org.matrix.android.sdk.api.crypto.VerificationState
|
||||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||||
@ -31,8 +32,7 @@ data class MessageInformationData(
|
|||||||
val ageLocalTS: Long?,
|
val ageLocalTS: Long?,
|
||||||
val avatarUrl: String?,
|
val avatarUrl: String?,
|
||||||
val memberName: CharSequence? = null,
|
val memberName: CharSequence? = null,
|
||||||
val showInformation: Boolean = true,
|
val messageLayout: TimelineMessageLayout,
|
||||||
val forceShowTimestamp: Boolean = false,
|
|
||||||
/*List of reactions (emoji,count,isSelected)*/
|
/*List of reactions (emoji,count,isSelected)*/
|
||||||
val orderedReactionList: List<ReactionInfoData>? = null,
|
val orderedReactionList: List<ReactionInfoData>? = null,
|
||||||
val pollResponseAggregatedSummary: PollResponseData? = null,
|
val pollResponseAggregatedSummary: PollResponseData? = null,
|
||||||
@ -41,7 +41,9 @@ data class MessageInformationData(
|
|||||||
val referencesInfoData: ReferencesInfoData? = null,
|
val referencesInfoData: ReferencesInfoData? = null,
|
||||||
val sentByMe: Boolean,
|
val sentByMe: Boolean,
|
||||||
val e2eDecoration: E2EDecoration = E2EDecoration.NONE,
|
val e2eDecoration: E2EDecoration = E2EDecoration.NONE,
|
||||||
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE
|
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE,
|
||||||
|
val isFirstFromThisSender: Boolean = false,
|
||||||
|
val isLastFromThisSender: Boolean = false
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
val matrixItem: MatrixItem
|
val matrixItem: MatrixItem
|
||||||
|
@ -17,12 +17,16 @@
|
|||||||
package im.vector.app.features.home.room.detail.timeline.item
|
package im.vector.app.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
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.R
|
||||||
import im.vector.app.core.glide.GlideApp
|
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.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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() {
|
abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() {
|
||||||
@ -34,17 +38,32 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
|
|||||||
var userId: String? = null
|
var userId: String? = null
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
|
var mapWidth: Int = 0
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var mapHeight: Int = 0
|
||||||
|
|
||||||
|
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||||
var locationPinProvider: LocationPinProvider? = null
|
var locationPinProvider: LocationPinProvider? = null
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
renderSendState(holder.view, null)
|
renderSendState(holder.view, null)
|
||||||
|
|
||||||
val location = locationUrl ?: return
|
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)
|
GlideApp.with(holder.staticMapImageView)
|
||||||
.load(location)
|
.load(location)
|
||||||
.apply(RequestOptions.centerCropTransform())
|
.transform(imageCornerTransformation)
|
||||||
.into(holder.staticMapImageView)
|
.into(holder.staticMapImageView)
|
||||||
|
|
||||||
locationPinProvider?.create(userId) { pinDrawable ->
|
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) {
|
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||||
val staticMapImageView by bind<ImageView>(R.id.staticMapImageView)
|
val staticMapImageView by bind<ImageView>(R.id.staticMapImageView)
|
||||||
|
@ -80,6 +80,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||||||
safePreviewUrlRetriever.addListener(attributes.informationData.eventId, previewUrlViewUpdater)
|
safePreviewUrlRetriever.addListener(attributes.informationData.eventId, previewUrlViewUpdater)
|
||||||
}
|
}
|
||||||
holder.previewUrlView.delegate = previewUrlCallback
|
holder.previewUrlView.delegate = previewUrlCallback
|
||||||
|
holder.previewUrlView.renderMessageLayout(attributes.informationData.messageLayout)
|
||||||
|
|
||||||
if (useBigFont) {
|
if (useBigFont) {
|
||||||
holder.messageView.textSize = 44F
|
holder.messageView.textSize = 44F
|
||||||
@ -121,7 +122,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||||||
previewUrlRetriever?.removeListener(attributes.informationData.eventId, previewUrlViewUpdater)
|
previewUrlRetriever?.removeListener(attributes.informationData.eventId, previewUrlViewUpdater)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||||
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
|
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
|
||||||
|
@ -16,7 +16,10 @@
|
|||||||
|
|
||||||
package im.vector.app.features.home.room.detail.timeline.item
|
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.text.format.DateUtils
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.TextView
|
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.ContentDownloadStateTrackerBinder
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
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.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)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
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) }
|
holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
|
||||||
|
|
||||||
voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener {
|
voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener {
|
||||||
@ -120,9 +131,10 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||||||
voiceMessagePlaybackTracker.unTrack(attributes.informationData.eventId)
|
voiceMessagePlaybackTracker.unTrack(attributes.informationData.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder(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 voiceLayout by bind<ViewGroup>(R.id.voiceLayout)
|
||||||
val voicePlaybackControlButton by bind<ImageButton>(R.id.voicePlaybackControlButton)
|
val voicePlaybackControlButton by bind<ImageButton>(R.id.voicePlaybackControlButton)
|
||||||
val voicePlaybackTime by bind<TextView>(R.id.voicePlaybackTime)
|
val voicePlaybackTime by bind<TextView>(R.id.voicePlaybackTime)
|
||||||
|
@ -64,7 +64,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
|||||||
return listOf(attributes.informationData.eventId)
|
return listOf(attributes.informationData.eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
class Holder : BaseHolder(STUB_ID) {
|
class Holder : BaseHolder(STUB_ID) {
|
||||||
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||||
|
@ -50,6 +50,8 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
|
|||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
lateinit var optionViewStates: List<PollOptionViewState>
|
lateinit var optionViewStates: List<PollOptionViewState>
|
||||||
|
|
||||||
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
val relatedEventId = eventId ?: return
|
val relatedEventId = eventId ?: return
|
||||||
|
@ -22,7 +22,7 @@ import im.vector.app.R
|
|||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||||
abstract class RedactedMessageItem : AbsMessageItem<RedactedMessageItem.Holder>() {
|
abstract class RedactedMessageItem : AbsMessageItem<RedactedMessageItem.Holder>() {
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
override fun shouldShowReactionAtBottom() = false
|
override fun shouldShowReactionAtBottom() = false
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem<StatusTileTimelineIte
|
|||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
lateinit var attributes: Attributes
|
lateinit var attributes: Attributes
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
|
@ -51,7 +51,7 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
|
|||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var callback: TimelineEventController.Callback? = null
|
var callback: TimelineEventController.Callback? = null
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
|
@ -41,7 +41,7 @@ abstract class WidgetTileTimelineItem : AbsBaseMessageItem<WidgetTileTimelineIte
|
|||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
lateinit var attributes: Attributes
|
lateinit var attributes: Attributes
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun bind(holder: Holder) {
|
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
|
package im.vector.app.features.home.room.detail.timeline.url
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.ColorStateList
|
||||||
|
import android.graphics.Color
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.google.android.material.card.MaterialCardView
|
import com.google.android.material.card.MaterialCardView
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.setTextOrHide
|
import im.vector.app.core.extensions.setTextOrHide
|
||||||
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
import im.vector.app.databinding.ViewUrlPreviewBinding
|
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.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.media.ImageContentRenderer
|
||||||
import im.vector.app.features.themes.ThemeUtils
|
import im.vector.app.features.themes.ThemeUtils
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
|
||||||
import org.matrix.android.sdk.api.session.media.PreviewUrlData
|
import org.matrix.android.sdk.api.session.media.PreviewUrlData
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,7 +41,7 @@ class PreviewUrlView @JvmOverloads constructor(
|
|||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0
|
defStyleAttr: Int = 0
|
||||||
) : MaterialCardView(context, attrs, defStyleAttr), View.OnClickListener {
|
) : MaterialCardView(context, attrs, defStyleAttr), View.OnClickListener, TimelineMessageLayoutRenderer {
|
||||||
|
|
||||||
private lateinit var views: ViewUrlPreviewBinding
|
private lateinit var views: ViewUrlPreviewBinding
|
||||||
|
|
||||||
@ -47,7 +51,6 @@ class PreviewUrlView @JvmOverloads constructor(
|
|||||||
setupView()
|
setupView()
|
||||||
radius = resources.getDimensionPixelSize(R.dimen.preview_url_view_corner_radius).toFloat()
|
radius = resources.getDimensionPixelSize(R.dimen.preview_url_view_corner_radius).toFloat()
|
||||||
cardElevation = 0f
|
cardElevation = 0f
|
||||||
setCardBackgroundColor(ThemeUtils.getColor(context, R.attr.vctr_system))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var state: PreviewUrlUiState = PreviewUrlUiState.Unknown
|
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?) {
|
override fun onClick(v: View?) {
|
||||||
when (val finalState = state) {
|
when (val finalState = state) {
|
||||||
is PreviewUrlUiState.Data -> delegate?.onPreviewUrlClicked(finalState.url)
|
is PreviewUrlUiState.Data -> delegate?.onPreviewUrlClicked(finalState.url)
|
||||||
@ -127,7 +146,7 @@ class PreviewUrlView @JvmOverloads constructor(
|
|||||||
isVisible = true
|
isVisible = true
|
||||||
|
|
||||||
views.urlPreviewTitle.setTextOrHide(previewUrlData.title)
|
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.setTextOrHide(previewUrlData.description)
|
||||||
views.urlPreviewDescription.maxLines = when {
|
views.urlPreviewDescription.maxLines = when {
|
||||||
previewUrlData.mxcUrl != null -> 2
|
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
|
package im.vector.app.features.html
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
import androidx.core.text.toSpannable
|
import androidx.core.text.toSpannable
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
import im.vector.app.features.settings.VectorPreferences
|
import im.vector.app.features.settings.VectorPreferences
|
||||||
import io.noties.markwon.AbstractMarkwonPlugin
|
import io.noties.markwon.AbstractMarkwonPlugin
|
||||||
import io.noties.markwon.Markwon
|
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
|
.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 {
|
override fun processMarkdown(markdown: String): String {
|
||||||
return markdown
|
return markdown
|
||||||
.replace(Regex("""<span\s+data-mx-maths="([^"]*)">.*?</span>""")) {
|
.replace(Regex("""<span\s+data-mx-maths="([^"]*)">.*?</span>""")) { matchResult ->
|
||||||
matchResult -> "$$" + matchResult.groupValues[1] + "$$"
|
"$$" + matchResult.groupValues[1] + "$$"
|
||||||
}
|
}
|
||||||
.replace(Regex("""<div\s+data-mx-maths="([^"]*)">.*?</div>""")) {
|
.replace(Regex("""<div\s+data-mx-maths="([^"]*)">.*?</div>""")) { matchResult ->
|
||||||
matchResult -> "\n$$\n" + matchResult.groupValues[1] + "\n$$\n"
|
"\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) {
|
override fun configureHtml(plugin: HtmlPlugin) {
|
||||||
plugin
|
plugin
|
||||||
.addHandler(FontTagHandler())
|
.addHandler(FontTagHandler())
|
||||||
|
.addHandler(ParagraphHandler(DimensionConverter(resources)))
|
||||||
.addHandler(MxReplyTagHandler())
|
.addHandler(MxReplyTagHandler())
|
||||||
|
.addHandler(CodePreTagHandler())
|
||||||
|
.addHandler(CodeTagHandler())
|
||||||
.addHandler(SpanHandler(colorProvider))
|
.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
|
package im.vector.app.features.html
|
||||||
|
|
||||||
import io.noties.markwon.MarkwonVisitor
|
import io.noties.markwon.MarkwonVisitor
|
||||||
import io.noties.markwon.SpannableBuilder
|
|
||||||
import io.noties.markwon.html.HtmlTag
|
import io.noties.markwon.html.HtmlTag
|
||||||
import io.noties.markwon.html.MarkwonHtmlRenderer
|
import io.noties.markwon.html.MarkwonHtmlRenderer
|
||||||
import io.noties.markwon.html.TagHandler
|
import io.noties.markwon.html.TagHandler
|
||||||
import org.commonmark.node.BlockQuote
|
|
||||||
|
|
||||||
class MxReplyTagHandler : TagHandler() {
|
class MxReplyTagHandler : TagHandler() {
|
||||||
|
|
||||||
override fun supportedTags() = listOf("mx-reply")
|
override fun supportedTags() = listOf("mx-reply")
|
||||||
|
|
||||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||||
val configuration = visitor.configuration()
|
visitChildren(visitor, renderer, tag.asBlock)
|
||||||
val factory = configuration.spansFactory().get(BlockQuote::class.java)
|
val replyText = visitor.builder().removeFromEnd(tag.end())
|
||||||
if (factory != null) {
|
visitor.builder().append("\n\n").append(replyText)
|
||||||
SpannableBuilder.setSpans(
|
|
||||||
visitor.builder(),
|
|
||||||
factory.getSpans(configuration, visitor.renderProps()),
|
|
||||||
tag.start(),
|
|
||||||
tag.end()
|
|
||||||
)
|
|
||||||
val replyText = visitor.builder().removeFromEnd(tag.end())
|
|
||||||
visitor.builder().append("\n\n").append(replyText)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
fm: Paint.FontMetricsInt?): Int {
|
||||||
val rect = pillDrawable.bounds
|
val rect = pillDrawable.bounds
|
||||||
if (fm != null) {
|
if (fm != null) {
|
||||||
fm.ascent = -rect.bottom
|
val fmPaint = paint.fontMetricsInt
|
||||||
fm.descent = 0
|
val fontHeight = fmPaint.bottom - fmPaint.top
|
||||||
fm.top = fm.ascent
|
val drHeight = rect.bottom - rect.top
|
||||||
fm.bottom = 0
|
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
|
return rect.right
|
||||||
}
|
}
|
||||||
@ -82,7 +87,9 @@ class PillImageSpan(private val glideRequests: GlideRequests,
|
|||||||
bottom: Int,
|
bottom: Int,
|
||||||
paint: Paint) {
|
paint: Paint) {
|
||||||
canvas.save()
|
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())
|
canvas.translate(x, transY.toFloat())
|
||||||
pillDrawable.draw(canvas)
|
pillDrawable.draw(canvas)
|
||||||
canvas.restore()
|
canvas.restore()
|
||||||
|
@ -16,13 +16,13 @@
|
|||||||
|
|
||||||
package im.vector.app.features.location
|
package im.vector.app.features.location
|
||||||
|
|
||||||
import android.content.res.Resources
|
|
||||||
import im.vector.app.BuildConfig
|
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
|
import javax.inject.Inject
|
||||||
|
|
||||||
class UrlMapProvider @Inject constructor(
|
class UrlMapProvider @Inject constructor(
|
||||||
private val resources: Resources
|
private val localeProvider: LocaleProvider
|
||||||
) {
|
) {
|
||||||
private val keyParam = "?key=${BuildConfig.mapTilerKey}"
|
private val keyParam = "?key=${BuildConfig.mapTilerKey}"
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ class UrlMapProvider @Inject constructor(
|
|||||||
append(height)
|
append(height)
|
||||||
append(".png")
|
append(".png")
|
||||||
append(keyParam)
|
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
|
// On LTR languages we want the legal mentions to be displayed on the bottom left of the image
|
||||||
append("&attribution=bottomleft")
|
append("&attribution=bottomleft")
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.media
|
package im.vector.app.features.media
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
@ -23,6 +24,7 @@ import android.view.View
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import com.bumptech.glide.load.DataSource
|
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.DiskCacheStrategy
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
@ -42,6 +44,7 @@ import im.vector.app.core.utils.DimensionConverter
|
|||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
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 org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -59,6 +62,9 @@ interface AttachmentData : Parcelable {
|
|||||||
val allowNonMxcUrls: Boolean
|
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,
|
class ImageContentRenderer @Inject constructor(private val localFilesHelper: LocalFilesHelper,
|
||||||
private val activeSessionHolder: ActiveSessionHolder,
|
private val activeSessionHolder: ActiveSessionHolder,
|
||||||
private val dimensionConverter: DimensionConverter) {
|
private val dimensionConverter: DimensionConverter) {
|
||||||
@ -87,12 +93,20 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
|
|||||||
/**
|
/**
|
||||||
* For url preview
|
* For url preview
|
||||||
*/
|
*/
|
||||||
fun render(mxcUrl: String, imageView: ImageView): Boolean {
|
fun render(previewUrlData: PreviewUrlData, imageView: ImageView): Boolean {
|
||||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
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)
|
GlideApp.with(imageView)
|
||||||
.load(imageUrl)
|
.load(imageUrl)
|
||||||
|
.override(width, height.coerceAtMost(maxHeight))
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -109,7 +123,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
|
|||||||
.into(imageView)
|
.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)
|
val size = processSize(data, mode)
|
||||||
imageView.updateLayoutParams {
|
imageView.updateLayoutParams {
|
||||||
width = size.width
|
width = size.width
|
||||||
@ -120,7 +134,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
|
|||||||
|
|
||||||
createGlideRequest(data, mode, imageView, size)
|
createGlideRequest(data, mode, imageView, size)
|
||||||
.dontAnimate()
|
.dontAnimate()
|
||||||
.transform(RoundedCorners(dimensionConverter.dpToPx(8)))
|
.transform(cornerTransformation)
|
||||||
// .thumbnail(0.3f)
|
// .thumbnail(0.3f)
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,7 @@ class ReactionButton @JvmOverloads constructor(context: Context,
|
|||||||
orientation = HORIZONTAL
|
orientation = HORIZONTAL
|
||||||
minimumHeight = DimensionConverter(context.resources).dpToPx(30)
|
minimumHeight = DimensionConverter(context.resources).dpToPx(30)
|
||||||
gravity = Gravity.CENTER
|
gravity = Gravity.CENTER
|
||||||
|
layoutDirection = View.LAYOUT_DIRECTION_LOCALE
|
||||||
views = ReactionButtonBinding.bind(this)
|
views = ReactionButtonBinding.bind(this)
|
||||||
views.reactionCount.text = TextUtils.formatCountToShortDecimal(reactionCount)
|
views.reactionCount.text = TextUtils.formatCountToShortDecimal(reactionCount)
|
||||||
context.withStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr) {
|
context.withStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr) {
|
||||||
|
@ -83,6 +83,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
|||||||
// interface
|
// interface
|
||||||
const val SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY = "SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY"
|
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_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"
|
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_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY"
|
||||||
private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_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)
|
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.
|
* Tells if the rage shake is used.
|
||||||
*
|
*
|
||||||
|
@ -53,7 +53,6 @@ class RoomWidgetPermissionBottomSheet :
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
setupViews()
|
setupViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
12
vector/src/main/res/drawable/bg_avatar_border.xml
Normal file
12
vector/src/main/res/drawable/bg_avatar_border.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
|
||||||
|
<solid android:color="@android:color/transparent"/>
|
||||||
|
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="?android:colorBackground"/>
|
||||||
|
|
||||||
|
</shape>
|
8
vector/src/main/res/drawable/overlay_bubble_media.xml
Normal file
8
vector/src/main/res/drawable/overlay_bubble_media.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<gradient
|
||||||
|
android:type="linear"
|
||||||
|
android:angle="270"
|
||||||
|
android:startColor="#00000000"
|
||||||
|
android:endColor="#33000000"/>
|
||||||
|
</shape>
|
@ -35,6 +35,7 @@
|
|||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textColor="?vctr_content_primary"
|
android:textColor="?vctr_content_primary"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
app:layout_constraintEnd_toStartOf="@id/bottom_sheet_message_preview_timestamp"
|
app:layout_constraintEnd_toStartOf="@id/bottom_sheet_message_preview_timestamp"
|
||||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||||
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
|
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
|
||||||
@ -78,6 +79,7 @@
|
|||||||
android:maxLines="3"
|
android:maxLines="3"
|
||||||
android:textColor="?vctr_content_secondary"
|
android:textColor="?vctr_content_secondary"
|
||||||
android:textIsSelectable="false"
|
android:textIsSelectable="false"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_body_details"
|
app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_body_details"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||||
@ -96,6 +98,7 @@
|
|||||||
android:textColor="?vctr_content_tertiary"
|
android:textColor="?vctr_content_tertiary"
|
||||||
android:textIsSelectable="false"
|
android:textIsSelectable="false"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="@id/bottom_sheet_message_preview_body"
|
app:layout_constraintEnd_toEndOf="@id/bottom_sheet_message_preview_body"
|
||||||
app:layout_constraintStart_toStartOf="@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_height="wrap_content"
|
||||||
android:layout_marginTop="3dp"
|
android:layout_marginTop="3dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="2"
|
android:maxLines="2"
|
||||||
android:textColor="?vctr_content_secondary"
|
android:textColor="?vctr_content_secondary"
|
||||||
@ -202,6 +203,7 @@
|
|||||||
android:id="@+id/roomTypingView"
|
android:id="@+id/roomTypingView"
|
||||||
style="@style/Widget.Vector.TextView.Body"
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="3dp"
|
android:layout_marginTop="3dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginEnd="4dp"
|
android:layout_marginEnd="4dp"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
android:layout_toStartOf="@id/messageTimeView"
|
android:layout_toStartOf="@id/messageTimeView"
|
||||||
android:layout_toEndOf="@id/messageStartGuideline"
|
android:layout_toEndOf="@id/messageStartGuideline"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
@ -76,66 +77,16 @@
|
|||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<FrameLayout
|
<include
|
||||||
android:id="@+id/viewStubContainer"
|
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_height="wrap_content"
|
||||||
android:layout_below="@id/messageMemberNameView"
|
android:layout_below="@id/messageMemberNameView"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_toStartOf="@id/messageSendStateImageView"
|
android:layout_toStartOf="@id/messageSendStateImageView"
|
||||||
android:layout_toEndOf="@id/messageStartGuideline"
|
android:layout_toEndOf="@id/messageStartGuideline"
|
||||||
android:addStatesFromChildren="true">
|
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>
|
|
||||||
|
|
||||||
<im.vector.app.core.ui.views.SendStateImageView
|
<im.vector.app.core.ui.views.SendStateImageView
|
||||||
android:id="@+id/messageSendStateImageView"
|
android:id="@+id/messageSendStateImageView"
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/viewStubContainer"
|
android:id="@+id/viewStubContainer"
|
||||||
|
style="@style/TimelineContentStubContainerParams"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentTop="true"
|
android:layout_alignParentTop="true"
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/viewStubContainer"
|
android:id="@+id/viewStubContainer"
|
||||||
|
style="@style/TimelineContentStubContainerParams"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentTop="true"
|
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
|
<TextView
|
||||||
android:id="@+id/itemDefaultTextView"
|
android:id="@+id/itemDefaultTextView"
|
||||||
style="@style/Widget.Vector.TextView.Body"
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="top"
|
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
|
@ -1,80 +1,62 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/messageFileLayout"
|
android:id="@+id/messageFileLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingTop="8dp"
|
android:orientation="vertical"
|
||||||
android:paddingBottom="8dp"
|
|
||||||
tools:viewBindingIgnore="true">
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<im.vector.app.core.ui.views.ShieldImageView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/messageFilee2eIcon"
|
android:id="@+id/messageFileMainLayout"
|
||||||
android:layout_width="14dp"
|
style="@style/TimelineContentMediaPillStyle"
|
||||||
android:layout_height="14dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<!-- the media type -->
|
|
||||||
<RelativeLayout
|
|
||||||
android:id="@+id/messageFileImageView"
|
|
||||||
android:layout_width="40dp"
|
|
||||||
android:layout_height="40dp"
|
|
||||||
android:layout_marginStart="4dp"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/messageFilee2eIcon"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<include layout="@layout/view_file_icon" />
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<!-- <ImageView-->
|
|
||||||
<!-- android:id="@+id/messageFileImageView"-->
|
|
||||||
<!-- android:layout_width="@dimen/chat_avatar_size"-->
|
|
||||||
<!-- android:layout_height="@dimen/chat_avatar_size"-->
|
|
||||||
<!-- android:layout_marginStart="4dp"-->
|
|
||||||
<!-- app:layout_constraintStart_toEndOf="@id/messageFilee2eIcon"-->
|
|
||||||
<!-- app:layout_constraintTop_toTopOf="parent"-->
|
|
||||||
<!-- tools:src="@drawable/filetype_attachment" />-->
|
|
||||||
|
|
||||||
<!-- the media -->
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/messageFilenameView"
|
|
||||||
style="@style/Widget.Vector.TextView.Body"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="4dp"
|
tools:viewBindingIgnore="true">
|
||||||
android:layout_marginEnd="32dp"
|
|
||||||
android:autoLink="none"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:minHeight="@dimen/chat_avatar_size"
|
|
||||||
app:layout_constrainedWidth="true"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintHorizontal_bias="0"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/messageFileImageView"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="A filename here" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<FrameLayout
|
||||||
android:id="@+id/horizontalBarrier"
|
android:id="@+id/messageFileImageView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="32dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="32dp"
|
||||||
app:barrierDirection="bottom"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:constraint_referenced_ids="messageFileImageView,messageFilenameView" />
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
<include layout="@layout/view_file_icon" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<!-- the file name-->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/messageFilenameView"
|
||||||
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:autoLink="none"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/messageFileImageView"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
tools:text="A filename here" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/messageFileUploadProgressLayout"
|
android:id="@+id/messageFileUploadProgressLayout"
|
||||||
layout="@layout/media_upload_download_progress_layout"
|
layout="@layout/media_upload_download_progress_layout"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="46dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginEnd="32dp"
|
android:layout_marginEnd="32dp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/horizontalBarrier"
|
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
</LinearLayout>
|
@ -1,16 +1,21 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
app:cardCornerRadius="8dp">
|
|
||||||
|
|
||||||
|
<!-- Size will be overrode -->
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/staticMapImageView"
|
android:id="@+id/staticMapImageView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="300dp"
|
||||||
android:layout_height="200dp"
|
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
|
<ImageView
|
||||||
android:id="@+id/staticMapPinImageView"
|
android:id="@+id/staticMapPinImageView"
|
||||||
@ -19,6 +24,10 @@
|
|||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginBottom="28dp"
|
android:layout_marginBottom="28dp"
|
||||||
android:importantForAccessibility="no"
|
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:id="@+id/messageThumbnailView"
|
||||||
android:layout_width="375dp"
|
android:layout_width="375dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginEnd="32dp"
|
|
||||||
android:contentDescription="@string/a11y_image"
|
android:contentDescription="@string/a11y_image"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
@ -25,6 +24,7 @@
|
|||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:contentDescription="@string/action_play"
|
android:contentDescription="@string/action_play"
|
||||||
android:src="@drawable/ic_material_play_circle"
|
android:src="@drawable/ic_material_play_circle"
|
||||||
|
app:tint="?vctr_system"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/messageThumbnailView"
|
app:layout_constraintBottom_toBottomOf="@id/messageThumbnailView"
|
||||||
app:layout_constraintEnd_toEndOf="@id/messageThumbnailView"
|
app:layout_constraintEnd_toEndOf="@id/messageThumbnailView"
|
||||||
|
@ -19,9 +19,8 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/itemNoticeTextView"
|
android:id="@+id/itemNoticeTextView"
|
||||||
style="@style/Widget.Vector.TextView.Body"
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="top"
|
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
|
@ -2,17 +2,19 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout 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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:minWidth="@dimen/chat_bubble_fixed_size"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/questionTextView"
|
android:id="@+id/questionTextView"
|
||||||
style="@style/Widget.Vector.TextView.Subtitle"
|
style="@style/Widget.Vector.TextView.Subtitle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:textColor="?vctr_content_primary"
|
android:textColor="?vctr_content_primary"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintHorizontal_bias="0"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
@ -17,11 +17,10 @@
|
|||||||
|
|
||||||
<im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView
|
<im.vector.app.features.home.room.detail.timeline.url.PreviewUrlView
|
||||||
android:id="@+id/messageUrlPreview"
|
android:id="@+id/messageUrlPreview"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginBottom="4dp"
|
android:layout_marginBottom="4dp"
|
||||||
android:foreground="?attr/selectableItemBackground"
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
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"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/voiceLayout"
|
android:id="@+id/voiceLayout"
|
||||||
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="@dimen/chat_bubble_fixed_size"
|
||||||
tools:viewBindingIgnore="true">
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/voicePlaybackLayout"
|
android:id="@+id/voicePlaybackLayout"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/bg_voice_playback"
|
style="@style/TimelineContentMediaPillStyle">
|
||||||
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">
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/voicePlaybackControlButton"
|
android:id="@+id/voicePlaybackControlButton"
|
||||||
android:layout_width="32dp"
|
android:layout_width="@dimen/item_event_message_media_button_size"
|
||||||
android:layout_height="32dp"
|
android:layout_height="@dimen/item_event_message_media_button_size"
|
||||||
android:background="@drawable/bg_voice_play_pause_button"
|
android:background="@drawable/bg_voice_play_pause_button"
|
||||||
android:backgroundTint="?android:colorBackground"
|
android:backgroundTint="?android:colorBackground"
|
||||||
android:contentDescription="@string/a11y_play_voice_message"
|
android:contentDescription="@string/a11y_play_voice_message"
|
||||||
@ -65,16 +58,12 @@
|
|||||||
<include
|
<include
|
||||||
android:id="@+id/messageFileUploadProgressLayout"
|
android:id="@+id/messageFileUploadProgressLayout"
|
||||||
layout="@layout/media_upload_download_progress_layout"
|
layout="@layout/media_upload_download_progress_layout"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="46dp"
|
android:layout_height="46dp"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginEnd="32dp"
|
android:layout_marginEnd="32dp"
|
||||||
android:visibility="gone"
|
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" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</LinearLayout>
|
||||||
|
@ -2,22 +2,22 @@
|
|||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="50dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="50dp"
|
android:layout_height="32dp"
|
||||||
tools:parentTag="android.widget.RelativeLayout">
|
tools:parentTag="android.widget.FrameLayout">
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/messageFileProgressbar"
|
android:id="@+id/messageFileProgressbar"
|
||||||
style="@style/Widget.Vector.ProgressBar.Horizontal.File"
|
style="@style/Widget.Vector.ProgressBar.Horizontal.File"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:progress="40" />
|
tools:progress="40" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/messageFileIconView"
|
android:id="@+id/messageFileIconView"
|
||||||
android:layout_width="20dp"
|
android:layout_width="16dp"
|
||||||
android:layout_height="20dp"
|
android:layout_height="16dp"
|
||||||
android:layout_centerInParent="true"
|
android:layout_gravity="center"
|
||||||
android:contentDescription="@string/attachment_type_file"
|
android:contentDescription="@string/attachment_type_file"
|
||||||
android:src="@drawable/ic_download"
|
android:src="@drawable/ic_download"
|
||||||
app:tint="?vctr_notice_secondary"
|
app:tint="?vctr_notice_secondary"
|
||||||
|
228
vector/src/main/res/layout/view_message_bubble.xml
Normal file
228
vector/src/main/res/layout/view_message_bubble.xml
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:parentTag="android.widget.RelativeLayout">
|
||||||
|
|
||||||
|
<im.vector.app.core.platform.CheckableView
|
||||||
|
android:id="@+id/messageSelectedBackground"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_alignBottom="@id/informationBottom"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:background="@drawable/highlighted_message_background" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/messageAvatarImageView"
|
||||||
|
android:layout_width="44dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="@drawable/bg_avatar_border"
|
||||||
|
android:contentDescription="@string/avatar"
|
||||||
|
android:elevation="2dp"
|
||||||
|
android:padding="2dp"
|
||||||
|
tools:src="@sample/user_round_avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/messageMemberNameView"
|
||||||
|
style="@style/Widget.Vector.TextView.Subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:layout_toEndOf="@id/messageStartGuideline"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textColor="?vctr_content_primary"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="@sample/users.json/data/displayName" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/messageStartGuideline"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
tools:layout_marginStart="52dp" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/messageEndGuideline"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginEnd="64dp" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/decorationSpace"
|
||||||
|
android:layout_width="4dp"
|
||||||
|
android:layout_height="8dp"
|
||||||
|
android:layout_toEndOf="@id/messageStartGuideline" />
|
||||||
|
|
||||||
|
<im.vector.app.core.ui.views.ShieldImageView
|
||||||
|
android:id="@+id/messageE2EDecoration"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_alignTop="@id/bubbleWrapper"
|
||||||
|
android:layout_alignEnd="@id/decorationSpace"
|
||||||
|
android:layout_marginTop="7dp"
|
||||||
|
android:elevation="2dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/bubbleWrapper"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/messageMemberNameView"
|
||||||
|
android:layout_toStartOf="@id/messageEndGuideline"
|
||||||
|
android:layout_toEndOf="@id/messageStartGuideline"
|
||||||
|
android:addStatesFromChildren="true"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/bubbleView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="0dp"
|
||||||
|
android:layout_marginEnd="0dp"
|
||||||
|
android:addStatesFromChildren="true"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/viewStubContainer"
|
||||||
|
layout="@layout/item_timeline_event_view_stubs_container"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:addStatesFromChildren="true"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintWidth_max="@dimen/chat_bubble_fixed_size" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/messageOverlayView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:background="@drawable/overlay_bubble_media"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/viewStubContainer"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/viewStubContainer"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/viewStubContainer"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/viewStubContainer"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/messageTimeView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:textColor="?vctr_content_tertiary"
|
||||||
|
android:textSize="10sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/viewStubContainer"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:text="@tools:sample/date/hhmm" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<im.vector.app.core.ui.views.SendStateImageView
|
||||||
|
android:id="@+id/messageSendStateImageView"
|
||||||
|
android:layout_width="@dimen/item_event_message_state_size"
|
||||||
|
android:layout_height="@dimen/item_event_message_state_size"
|
||||||
|
android:layout_alignBottom="@id/bubbleWrapper"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:contentDescription="@string/event_status_a11y_sending"
|
||||||
|
android:src="@drawable/ic_sending_message"
|
||||||
|
android:visibility="invisible"
|
||||||
|
tools:tint="?vctr_content_tertiary"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/eventSendingIndicator"
|
||||||
|
android:layout_width="@dimen/item_event_message_state_size"
|
||||||
|
android:layout_height="@dimen/item_event_message_state_size"
|
||||||
|
android:layout_alignBottom="@id/bubbleWrapper"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:indeterminateTint="?vctr_content_secondary"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:tint="?vctr_content_tertiary"
|
||||||
|
tools:ignore="MissingPrefix"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/informationBottom"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/bubbleWrapper"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:layout_marginBottom="2dp"
|
||||||
|
android:layout_toStartOf="@id/messageEndGuideline"
|
||||||
|
android:layout_toEndOf="@id/messageStartGuideline"
|
||||||
|
android:addStatesFromChildren="true"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.flexbox.FlexboxLayout
|
||||||
|
android:id="@+id/reactionsContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
app:dividerDrawable="@drawable/reaction_divider"
|
||||||
|
app:flexWrap="wrap"
|
||||||
|
app:showDivider="middle"
|
||||||
|
tools:background="#F0E0F0"
|
||||||
|
tools:layout_height="40dp">
|
||||||
|
|
||||||
|
<!-- ReactionButtons will be added here in the code -->
|
||||||
|
<!--im.vector.app.features.reactions.widget.ReactionButton
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" /-->
|
||||||
|
|
||||||
|
</com.google.android.flexbox.FlexboxLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/messageThreadSummaryContainer"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/informationBottom"
|
||||||
|
android:layout_toStartOf="@id/messageEndGuideline"
|
||||||
|
android:layout_toEndOf="@id/messageStartGuideline"
|
||||||
|
android:contentDescription="@string/room_threads_filter">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/messageThreadSummaryConstraintLayout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:background="@drawable/rounded_rect_shape_8"
|
||||||
|
android:contentDescription="@string/room_threads_filter"
|
||||||
|
android:layoutDirection="locale"
|
||||||
|
android:maxWidth="496dp"
|
||||||
|
android:minWidth="144dp"
|
||||||
|
android:paddingStart="13dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingEnd="13dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<include layout="@layout/view_thread_room_summary" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</merge>
|
@ -67,6 +67,7 @@
|
|||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
android:textAppearance="@style/TextAppearance.Vector.Widget.ActionBarTitle"
|
android:textAppearance="@style/TextAppearance.Vector.Widget.ActionBarTitle"
|
||||||
app:layout_constraintBottom_toTopOf="@id/roomToolbarSubtitleView"
|
app:layout_constraintBottom_toTopOf="@id/roomToolbarSubtitleView"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
@ -85,6 +86,7 @@
|
|||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
android:textAppearance="@style/TextAppearance.Vector.Widget.ActionBarSubTitle"
|
android:textAppearance="@style/TextAppearance.Vector.Widget.ActionBarSubTitle"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
@ -1,77 +1,102 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
<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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/informationUrlPreviewContainer"
|
android:id="@+id/informationUrlPreviewContainer"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
tools:parentTag="com.google.android.material.card.MaterialCardView">
|
tools:parentTag="com.google.android.material.card.MaterialCardView">
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="208dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!--Image dimensions will be overrode by ImageContentRenderer -->
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/url_preview_image"
|
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_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:adjustViewBounds="true"
|
android:orientation="vertical"
|
||||||
android:importantForAccessibility="no"
|
app:layout_constraintGuide_begin="8dp" />
|
||||||
android:maxHeight="200dp"
|
|
||||||
android:scaleType="fitXY"
|
|
||||||
tools:src="@tools:sample/backgrounds/scenic" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/url_preview_site"
|
android:id="@+id/url_preview_site"
|
||||||
style="@style/Widget.Vector.TextView.Caption"
|
style="@style/Widget.Vector.TextView.Caption"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/url_preview_close"
|
||||||
android:textColor="?vctr_content_secondary"
|
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" />
|
tools:text="BBC News" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/url_preview_title"
|
android:id="@+id/url_preview_title"
|
||||||
style="@style/Widget.Vector.TextView.Body.Medium"
|
style="@style/Widget.Vector.TextView.Body.Medium"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginEnd="@dimen/layout_touch_size"
|
android:layout_marginEnd="8dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="2"
|
android:maxLines="2"
|
||||||
android:textColor="?vctr_content_primary"
|
android:textColor="?vctr_content_primary"
|
||||||
android:textStyle="bold"
|
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" />
|
tools:text="Jo Malone denounces her former brand's John Boyega decision" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/url_preview_description"
|
android:id="@+id/url_preview_description"
|
||||||
style="@style/Widget.Vector.TextView.Body"
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:textColor="?vctr_content_secondary"
|
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”." />
|
tools:text="The British perfumer says removing actor John Boyega from his own advert was “utterly despicable”." />
|
||||||
|
|
||||||
</LinearLayout>
|
<ImageView
|
||||||
|
android:id="@+id/url_preview_close"
|
||||||
|
android:layout_width="@dimen/layout_touch_size"
|
||||||
|
android:layout_height="@dimen/layout_touch_size"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:contentDescription="@string/action_close"
|
||||||
|
android:scaleType="center"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
android:src="@drawable/ic_close_with_circular_bg"
|
||||||
|
tools:ignore="MissingPrefix" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/url_preview_close"
|
|
||||||
android:layout_width="@dimen/layout_touch_size"
|
|
||||||
android:layout_height="@dimen/layout_touch_size"
|
|
||||||
android:layout_gravity="top|end"
|
|
||||||
android:contentDescription="@string/action_close"
|
|
||||||
android:scaleType="center"
|
|
||||||
android:src="@drawable/ic_close_with_circular_bg"
|
|
||||||
tools:ignore="MissingPrefix" />
|
|
||||||
|
|
||||||
</merge>
|
</merge>
|
@ -160,7 +160,7 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:background="@drawable/bg_voice_playback"
|
android:background="@drawable/bg_media_pill"
|
||||||
android:backgroundTint="?vctr_content_quinary"
|
android:backgroundTint="?vctr_content_quinary"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="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="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="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_photo">Open camera</string>
|
||||||
<string name="tooltip_attachment_gallery">Send images and videos</string>
|
<string name="tooltip_attachment_gallery">Send images and videos</string>
|
||||||
<string name="tooltip_attachment_file">Upload file</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.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
|
<im.vector.app.core.preference.VectorSwitchPreference
|
||||||
android:defaultValue="true"
|
android:defaultValue="true"
|
||||||
android:key="SETTINGS_SHOW_URL_PREVIEW_KEY"
|
android:key="SETTINGS_SHOW_URL_PREVIEW_KEY"
|
||||||
|
Loading…
Reference in New Issue
Block a user