Merge branch 'develop' into feature/ons/fix_failed_maps_rendering
* develop: (54 commits) Bubbles: add CHANGELOG file Bubble: get LayoutDirection (RTL) from current Locale Version++ fastlane towncrier Version 1.3.18 changelog Improve missing state event detection to missing state events only one joined rooms (ignore LEFT room) Should reduce the number of initial sync Co-authors: ganfra and billcarsonfr Changelog added. taking the use case screen into account when accessing the sign up flows in the sanity tests updating copy split to match designs applying design feedback promoting use case strings for translation enabling the use case feature by default Code review fixes. ktlint Bubbles: clean up after review Sync: avoid deleting root event of CurrentState on gappy sync Code review fixes. Support generic location pin. ... # Conflicts: # vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageLocationItem.kt # vector/src/main/res/layout/item_timeline_event_location_stub.xml
This commit is contained in:
commit
fbc8866394
@ -1,3 +1,11 @@
|
|||||||
|
Changes in Element v1.3.18 (2022-02-03)
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
Bugfixes 🐛
|
||||||
|
----------
|
||||||
|
- Avoid deleting root event of CurrentState on gappy sync. In order to restore lost Events an initial sync may be triggered. ([#5137](https://github.com/vector-im/element-android/issues/5137))
|
||||||
|
|
||||||
|
|
||||||
Changes in Element v1.3.17 (2022-01-31)
|
Changes in Element v1.3.17 (2022-01-31)
|
||||||
=======================================
|
=======================================
|
||||||
|
|
||||||
|
1
changelog.d/4937.feature
Normal file
1
changelog.d/4937.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Support message bubbles in timeline.
|
1
changelog.d/5146.feature
Normal file
1
changelog.d/5146.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Support generic location pin
|
2
fastlane/metadata/android/en-US/changelogs/40103180.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/40103180.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
Main changes in this version: send your location to any room. Edit poll.
|
||||||
|
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.3.18
|
@ -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>
|
||||||
|
@ -31,7 +31,7 @@ android {
|
|||||||
// that the app's state is completely cleared between tests.
|
// that the app's state is completely cleared between tests.
|
||||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||||
|
|
||||||
buildConfigField "String", "SDK_VERSION", "\"1.3.18\""
|
buildConfigField "String", "SDK_VERSION", "\"1.3.19\""
|
||||||
|
|
||||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||||
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
||||||
|
@ -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?
|
||||||
)
|
)
|
||||||
|
@ -64,4 +64,12 @@ data class MessageLocationContent(
|
|||||||
) : MessageContent {
|
) : MessageContent {
|
||||||
|
|
||||||
fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
|
fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the location asset is a user location, not a generic one.
|
||||||
|
*/
|
||||||
|
fun isSelfLocation(): Boolean {
|
||||||
|
// Should behave like m.self if locationAsset is null
|
||||||
|
return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
) : RealmMigration {
|
) : RealmMigration {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val SESSION_STORE_SCHEMA_VERSION = 22L
|
const val SESSION_STORE_SCHEMA_VERSION = 24L
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,6 +92,8 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
if (oldVersion <= 19) migrateTo20(realm)
|
if (oldVersion <= 19) migrateTo20(realm)
|
||||||
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 <= 23) migrateTo24(realm)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo1(realm: DynamicRealm) {
|
private fun migrateTo1(realm: DynamicRealm) {
|
||||||
@ -450,6 +452,22 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
|
|
||||||
private fun migrateTo22(realm: DynamicRealm) {
|
private fun migrateTo22(realm: DynamicRealm) {
|
||||||
Timber.d("Step 21 -> 22")
|
Timber.d("Step 21 -> 22")
|
||||||
|
val listJoinedRoomIds = realm.where("RoomEntity")
|
||||||
|
.equalTo(RoomEntityFields.MEMBERSHIP_STR, Membership.JOIN.name).findAll()
|
||||||
|
.map { it.getString(RoomEntityFields.ROOM_ID) }
|
||||||
|
|
||||||
|
val hasMissingStateEvent = realm.where("CurrentStateEventEntity")
|
||||||
|
.`in`(CurrentStateEventEntityFields.ROOM_ID, listJoinedRoomIds.toTypedArray())
|
||||||
|
.isNull(CurrentStateEventEntityFields.ROOT.`$`).findFirst() != null
|
||||||
|
|
||||||
|
if (hasMissingStateEvent) {
|
||||||
|
Timber.v("Has some missing state event, clear session cache")
|
||||||
|
realm.deleteAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateTo23(realm: DynamicRealm) {
|
||||||
|
Timber.d("Step 22 -> 23")
|
||||||
val eventEntity = realm.schema.get("TimelineEventEntity") ?: return
|
val eventEntity = realm.schema.get("TimelineEventEntity") ?: return
|
||||||
|
|
||||||
realm.schema.get("EventEntity")
|
realm.schema.get("EventEntity")
|
||||||
@ -462,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,9 @@ internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRo
|
|||||||
if (deleteStateEvents) {
|
if (deleteStateEvents) {
|
||||||
stateEvents.deleteAllFromRealm()
|
stateEvents.deleteAllFromRealm()
|
||||||
}
|
}
|
||||||
timelineEvents.clearWith { it.deleteOnCascade(canDeleteRoot) }
|
timelineEvents.clearWith {
|
||||||
|
val deleteRoot = canDeleteRoot && (it.root?.stateKey == null || deleteStateEvents)
|
||||||
|
it.deleteOnCascade(deleteRoot)
|
||||||
|
}
|
||||||
deleteFromRealm()
|
deleteFromRealm()
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
@ -354,7 +354,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
|||||||
aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
|
aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
|
||||||
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
|
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
|
||||||
if (isLimited && lastChunk != null) {
|
if (isLimited && lastChunk != null) {
|
||||||
lastChunk.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true)
|
lastChunk.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = true)
|
||||||
}
|
}
|
||||||
val chunkEntity = if (!isLimited && lastChunk != null) {
|
val chunkEntity = if (!isLimited && lastChunk != null) {
|
||||||
lastChunk
|
lastChunk
|
||||||
|
@ -18,7 +18,7 @@ ext.versionMinor = 3
|
|||||||
// Note: even values are reserved for regular release, odd values for hotfix release.
|
// Note: even values are reserved for regular release, odd values for hotfix release.
|
||||||
// When creating a hotfix, you should decrease the value, since the current value
|
// When creating a hotfix, you should decrease the value, since the current value
|
||||||
// is the value for the next regular release.
|
// is the value for the next regular release.
|
||||||
ext.versionPatch = 18
|
ext.versionPatch = 19
|
||||||
|
|
||||||
static def getGitTimestamp() {
|
static def getGitTimestamp() {
|
||||||
def cmd = 'git show -s --format=%ct'
|
def cmd = 'git show -s --format=%ct'
|
||||||
|
@ -40,8 +40,11 @@ class OnboardingRobot {
|
|||||||
|
|
||||||
private fun crawlGetStarted() {
|
private fun crawlGetStarted() {
|
||||||
clickOn(R.id.loginSplashSubmit)
|
clickOn(R.id.loginSplashSubmit)
|
||||||
|
assertDisplayed(R.id.useCaseHeaderTitle, R.string.ftue_auth_use_case_title)
|
||||||
|
clickOn(R.id.useCaseOptionOne)
|
||||||
OnboardingServersRobot().crawlSignUp()
|
OnboardingServersRobot().crawlSignUp()
|
||||||
pressBack()
|
pressBack()
|
||||||
|
pressBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun crawlAlreadyHaveAccount() {
|
private fun crawlAlreadyHaveAccount() {
|
||||||
@ -66,6 +69,7 @@ class OnboardingRobot {
|
|||||||
assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_create_account)
|
assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_create_account)
|
||||||
if (createAccount) {
|
if (createAccount) {
|
||||||
clickOn(R.id.loginSplashSubmit)
|
clickOn(R.id.loginSplashSubmit)
|
||||||
|
clickOn(R.id.useCaseOptionOne)
|
||||||
} else {
|
} else {
|
||||||
clickOn(R.id.loginSplashAlreadyHaveAccount)
|
clickOn(R.id.loginSplashAlreadyHaveAccount)
|
||||||
}
|
}
|
||||||
|
@ -76,6 +76,9 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
|
|||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var locationPinProvider: LocationPinProvider? = null
|
var locationPinProvider: LocationPinProvider? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var locationOwnerId: String? = null
|
||||||
|
|
||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var movementMethod: MovementMethod? = null
|
var movementMethod: MovementMethod? = null
|
||||||
|
|
||||||
@ -109,7 +112,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
|
|||||||
.apply(RequestOptions.centerCropTransform())
|
.apply(RequestOptions.centerCropTransform())
|
||||||
.into(holder.staticMapImageView)
|
.into(holder.staticMapImageView)
|
||||||
|
|
||||||
locationPinProvider?.create(matrixItem.id) { pinDrawable ->
|
locationPinProvider?.create(locationOwnerId) { pinDrawable ->
|
||||||
GlideApp.with(holder.staticMapPinImageView)
|
GlideApp.with(holder.staticMapPinImageView)
|
||||||
.load(pinDrawable)
|
.load(pinDrawable)
|
||||||
.into(holder.staticMapPinImageView)
|
.into(holder.staticMapPinImageView)
|
||||||
|
@ -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
|
||||||
|
@ -36,5 +36,5 @@ class DefaultVectorFeatures : VectorFeatures {
|
|||||||
override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT
|
override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT
|
||||||
override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true
|
override fun isOnboardingAlreadyHaveAccountSplashEnabled() = true
|
||||||
override fun isOnboardingSplashCarouselEnabled() = true
|
override fun isOnboardingSplashCarouselEnabled() = true
|
||||||
override fun isOnboardingUseCaseEnabled() = false
|
override fun isOnboardingUseCaseEnabled() = true
|
||||||
}
|
}
|
||||||
|
@ -612,13 +612,14 @@ class TimelineFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShowLocationPreview(locationContent: MessageLocationContent, senderId: String) {
|
private fun handleShowLocationPreview(locationContent: MessageLocationContent, senderId: String) {
|
||||||
|
val isSelfLocation = locationContent.isSelfLocation()
|
||||||
navigator
|
navigator
|
||||||
.openLocationSharing(
|
.openLocationSharing(
|
||||||
context = requireContext(),
|
context = requireContext(),
|
||||||
roomId = timelineArgs.roomId,
|
roomId = timelineArgs.roomId,
|
||||||
mode = LocationSharingMode.PREVIEW,
|
mode = LocationSharingMode.PREVIEW,
|
||||||
initialLocationData = locationContent.toLocationData(),
|
initialLocationData = locationContent.toLocationData(),
|
||||||
locationOwnerId = senderId
|
locationOwnerId = if (isSelfLocation) senderId else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,7 +382,16 @@ 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)
|
||||||
|
// Should be build if not cached or if model should be refreshed
|
||||||
|
if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState) == false) {
|
||||||
val prevEvent = currentSnapshot.prevOrNull(position)
|
val 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 {
|
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
|
||||||
timelineEventVisibilityHelper.shouldShowEvent(
|
timelineEventVisibilityHelper.shouldShowEvent(
|
||||||
timelineEvent = it,
|
timelineEvent = it,
|
||||||
@ -390,12 +399,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||||||
isFromThreadTimeline = partialState.isFromThreadTimeline(),
|
isFromThreadTimeline = partialState.isFromThreadTimeline(),
|
||||||
rootThreadEventId = partialState.rootThreadEventId)
|
rootThreadEventId = partialState.rootThreadEventId)
|
||||||
}
|
}
|
||||||
// Should be build if not cached or if model should be refreshed
|
|
||||||
if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState) == false) {
|
|
||||||
val timelineEventsGroup = timelineEventsGroups.getOrNull(event)
|
val 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,
|
||||||
|
@ -45,6 +45,7 @@ import im.vector.app.features.location.toLocationData
|
|||||||
import im.vector.app.features.media.ImageContentRenderer
|
import im.vector.app.features.media.ImageContentRenderer
|
||||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
|
import org.matrix.android.sdk.api.extensions.orTrue
|
||||||
import org.matrix.android.sdk.api.failure.Failure
|
import org.matrix.android.sdk.api.failure.Failure
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
|
import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
|
||||||
@ -77,10 +78,12 @@ class MessageActionsEpoxyController @Inject constructor(
|
|||||||
val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL)
|
val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL)
|
||||||
val body = state.messageBody.linkify(host.listener)
|
val body = state.messageBody.linkify(host.listener)
|
||||||
val bindingOptions = spanUtils.getBindingOptions(body)
|
val bindingOptions = spanUtils.getBindingOptions(body)
|
||||||
val locationUrl = state.timelineEvent()?.root?.getClearContent()
|
|
||||||
|
val locationContent = state.timelineEvent()?.root?.getClearContent()
|
||||||
?.toModel<MessageLocationContent>(catchError = true)
|
?.toModel<MessageLocationContent>(catchError = true)
|
||||||
?.toLocationData()
|
val locationUrl = locationContent?.toLocationData()
|
||||||
?.let { urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, 1200, 800) }
|
?.let { urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, 1200, 800) }
|
||||||
|
val locationOwnerId = if (locationContent?.isSelfLocation().orTrue()) state.informationData.matrixItem.id else null
|
||||||
|
|
||||||
bottomSheetMessagePreviewItem {
|
bottomSheetMessagePreviewItem {
|
||||||
id("preview")
|
id("preview")
|
||||||
@ -96,6 +99,7 @@ class MessageActionsEpoxyController @Inject constructor(
|
|||||||
time(formattedDate)
|
time(formattedDate)
|
||||||
locationUrl(locationUrl)
|
locationUrl(locationUrl)
|
||||||
locationPinProvider(host.locationPinProvider)
|
locationPinProvider(host.locationPinProvider)
|
||||||
|
locationOwnerId(locationOwnerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send state
|
// Send state
|
||||||
|
@ -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,23 +200,30 @@ 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 {
|
||||||
urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height)
|
urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val userId = if (locationContent.isSelfLocation()) informationData.senderId else null
|
||||||
|
|
||||||
return MessageLocationItem_()
|
return MessageLocationItem_()
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
.locationUrl(locationUrl)
|
.locationUrl(locationUrl)
|
||||||
.userId(informationData.senderId)
|
.mapWidth(width)
|
||||||
|
.mapHeight(height)
|
||||||
|
.userId(userId)
|
||||||
.locationPinProvider(locationPinProvider)
|
.locationPinProvider(locationPinProvider)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
@ -524,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,
|
||||||
@ -596,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 {
|
||||||
@ -719,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,17 @@ class LocationPinProvider @Inject constructor(
|
|||||||
GlideApp.with(context)
|
GlideApp.with(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun create(userId: String, callback: (Drawable) -> Unit) {
|
/**
|
||||||
|
* Creates a pin drawable. If userId is null then a generic pin drawable will be created.
|
||||||
|
* @param userId userId that will be used to retrieve user avatar
|
||||||
|
* @param callback Pin drawable will be sent through the callback
|
||||||
|
*/
|
||||||
|
fun create(userId: String?, callback: (Drawable) -> Unit) {
|
||||||
|
if (userId == null) {
|
||||||
|
callback(ContextCompat.getDrawable(context, R.drawable.ic_location_pin)!!)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (cache.contains(userId)) {
|
if (cache.contains(userId)) {
|
||||||
callback(cache[userId]!!)
|
callback(cache[userId]!!)
|
||||||
return
|
return
|
||||||
|
@ -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.avatarImageView.setOnLongClickListener(null)
|
||||||
|
holder.avatarImageView.isVisible = false
|
||||||
|
}
|
||||||
|
if (attributes.informationData.messageLayout.showDisplayName) {
|
||||||
|
holder.memberNameView.isVisible = true
|
||||||
|
holder.memberNameView.text = attributes.informationData.memberName
|
||||||
|
holder.memberNameView.setTextColor(attributes.getMemberNameColor())
|
||||||
|
holder.memberNameView.onClick(_memberNameClickListener)
|
||||||
|
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||||
|
} else {
|
||||||
holder.memberNameView.setOnClickListener(null)
|
holder.memberNameView.setOnClickListener(null)
|
||||||
holder.avatarImageView.visibility = View.GONE
|
holder.memberNameView.setOnLongClickListener(null)
|
||||||
if (attributes.informationData.forceShowTimestamp) {
|
holder.memberNameView.isVisible = false
|
||||||
holder.memberNameView.isInvisible = true
|
}
|
||||||
|
if (attributes.informationData.messageLayout.showTimestamp) {
|
||||||
holder.timeView.isVisible = true
|
holder.timeView.isVisible = true
|
||||||
holder.timeView.text = attributes.informationData.time
|
holder.timeView.text = attributes.informationData.time
|
||||||
} else {
|
} else {
|
||||||
holder.memberNameView.isVisible = false
|
|
||||||
holder.timeView.isVisible = false
|
holder.timeView.isVisible = false
|
||||||
}
|
}
|
||||||
holder.avatarImageView.setOnLongClickListener(null)
|
|
||||||
holder.memberNameView.setOnLongClickListener(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -20,16 +20,21 @@ import android.graphics.drawable.Drawable
|
|||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
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.load.DataSource
|
import com.bumptech.glide.load.DataSource
|
||||||
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.request.RequestListener
|
import com.bumptech.glide.request.RequestListener
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
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>() {
|
||||||
@ -41,15 +46,29 @@ 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 locationOwnerId = userId ?: 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())
|
.apply(RequestOptions.centerCropTransform())
|
||||||
@ -61,7 +80,7 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
|
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
|
||||||
locationPinProvider?.create(locationOwnerId) { pinDrawable ->
|
locationPinProvider?.create(userId) { pinDrawable ->
|
||||||
GlideApp.with(holder.staticMapPinImageView)
|
GlideApp.with(holder.staticMapPinImageView)
|
||||||
.load(pinDrawable)
|
.load(pinDrawable)
|
||||||
.into(holder.staticMapPinImageView)
|
.into(holder.staticMapPinImageView)
|
||||||
@ -70,10 +89,11 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.transform(imageCornerTransformation)
|
||||||
.into(holder.staticMapImageView)
|
.into(holder.staticMapImageView)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
if (factory != null) {
|
|
||||||
SpannableBuilder.setSpans(
|
|
||||||
visitor.builder(),
|
|
||||||
factory.getSpans(configuration, visitor.renderProps()),
|
|
||||||
tag.start(),
|
|
||||||
tag.end()
|
|
||||||
)
|
|
||||||
val replyText = visitor.builder().removeFromEnd(tag.end())
|
val replyText = visitor.builder().removeFromEnd(tag.end())
|
||||||
visitor.builder().append("\n\n").append(replyText)
|
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()
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.location
|
|||||||
|
|
||||||
const val MAP_BASE_URL = "https://api.maptiler.com/maps/streets/style.json"
|
const val MAP_BASE_URL = "https://api.maptiler.com/maps/streets/style.json"
|
||||||
const val STATIC_MAP_BASE_URL = "https://api.maptiler.com/maps/basic/static/"
|
const val STATIC_MAP_BASE_URL = "https://api.maptiler.com/maps/basic/static/"
|
||||||
|
const val DEFAULT_PIN_ID = "DEFAULT_PIN_ID"
|
||||||
|
|
||||||
const val INITIAL_MAP_ZOOM_IN_PREVIEW = 15.0
|
const val INITIAL_MAP_ZOOM_IN_PREVIEW = 15.0
|
||||||
const val INITIAL_MAP_ZOOM_IN_TIMELINE = 17.0
|
const val INITIAL_MAP_ZOOM_IN_TIMELINE = 17.0
|
||||||
|
@ -121,7 +121,7 @@ class LocationPreviewFragment @Inject constructor(
|
|||||||
MapState(
|
MapState(
|
||||||
zoomOnlyOnce = true,
|
zoomOnlyOnce = true,
|
||||||
pinLocationData = location,
|
pinLocationData = location,
|
||||||
pinId = args.locationOwnerId,
|
pinId = args.locationOwnerId ?: DEFAULT_PIN_ID,
|
||||||
pinDrawable = pinDrawable
|
pinDrawable = pinDrawable
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -30,7 +30,7 @@ data class LocationSharingArgs(
|
|||||||
val roomId: String,
|
val roomId: String,
|
||||||
val mode: LocationSharingMode,
|
val mode: LocationSharingMode,
|
||||||
val initialLocationData: LocationData?,
|
val initialLocationData: LocationData?,
|
||||||
val locationOwnerId: String
|
val locationOwnerId: String?
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
@ -118,8 +118,4 @@ class LocationSharingFragment @Inject constructor(
|
|||||||
views.mapView.render(state.toMapState())
|
views.mapView.render(state.toMapState())
|
||||||
views.shareLocationGpsLoading.isGone = state.lastKnownLocation != null
|
views.shareLocationGpsLoading.isGone = state.lastKnownLocation != null
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val USER_PIN_NAME = "USER_PIN_NAME"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,6 @@ data class LocationSharingViewState(
|
|||||||
fun LocationSharingViewState.toMapState() = MapState(
|
fun LocationSharingViewState.toMapState() = MapState(
|
||||||
zoomOnlyOnce = true,
|
zoomOnlyOnce = true,
|
||||||
pinLocationData = lastKnownLocation,
|
pinLocationData = lastKnownLocation,
|
||||||
pinId = LocationSharingFragment.USER_PIN_NAME,
|
pinId = DEFAULT_PIN_ID,
|
||||||
pinDrawable = pinDrawable
|
pinDrawable = pinDrawable
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -556,7 +556,7 @@ class DefaultNavigator @Inject constructor(
|
|||||||
roomId: String,
|
roomId: String,
|
||||||
mode: LocationSharingMode,
|
mode: LocationSharingMode,
|
||||||
initialLocationData: LocationData?,
|
initialLocationData: LocationData?,
|
||||||
locationOwnerId: String) {
|
locationOwnerId: String?) {
|
||||||
val intent = LocationSharingActivity.getIntent(
|
val intent = LocationSharingActivity.getIntent(
|
||||||
context,
|
context,
|
||||||
LocationSharingArgs(roomId = roomId, mode = mode, initialLocationData = initialLocationData, locationOwnerId = locationOwnerId)
|
LocationSharingArgs(roomId = roomId, mode = mode, initialLocationData = initialLocationData, locationOwnerId = locationOwnerId)
|
||||||
|
@ -162,7 +162,8 @@ interface Navigator {
|
|||||||
roomId: String,
|
roomId: String,
|
||||||
mode: LocationSharingMode,
|
mode: LocationSharingMode,
|
||||||
initialLocationData: LocationData?,
|
initialLocationData: LocationData?,
|
||||||
locationOwnerId: String)
|
locationOwnerId: String?)
|
||||||
|
|
||||||
fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null)
|
fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null)
|
||||||
|
|
||||||
fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs)
|
fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs)
|
||||||
|
@ -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>
|
13
vector/src/main/res/drawable/ic_location_pin.xml
Normal file
13
vector/src/main/res/drawable/ic_location_pin.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="51dp"
|
||||||
|
android:height="54dp"
|
||||||
|
android:viewportWidth="51"
|
||||||
|
android:viewportHeight="54">
|
||||||
|
<path
|
||||||
|
android:pathData="M27.2956,44.2191C37.5577,42.7292 45.4403,33.8952 45.4403,23.2202C45.4403,11.5006 35.9397,2 24.2202,2C12.5006,2 3,11.5006 3,23.2202C3,33.8953 10.8827,42.7293 21.1449,44.2191L24.2202,47.1784L27.2956,44.2191Z"
|
||||||
|
android:fillColor="#0DBD8B"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M23.8041,15.073C20.5837,15.073 17.979,17.7486 17.979,21.0567C17.979,24.6213 21.6572,29.5365 23.1717,31.4085C23.5046,31.8188 24.112,31.8188 24.4449,31.4085C25.9511,29.5365 29.6293,24.6213 29.6293,21.0567C29.6293,17.7486 27.0246,15.073 23.8041,15.073ZM23.8041,23.1937C22.6558,23.1937 21.7237,22.2364 21.7237,21.0567C21.7237,19.8771 22.6558,18.9197 23.8041,18.9197C24.9525,18.9197 25.8846,19.8771 25.8846,21.0567C25.8846,22.2364 24.9525,23.1937 23.8041,23.1937Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
</vector>
|
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>
|
@ -79,7 +79,7 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/useCaseOptionOne"
|
android:id="@+id/useCaseOptionOne"
|
||||||
style="@style/Widget.Vector.TextView.Subtitle"
|
style="@style/Widget.Vector.TextView.Subtitle.Medium"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
@ -97,7 +97,7 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/useCaseOptionTwo"
|
android:id="@+id/useCaseOptionTwo"
|
||||||
style="@style/Widget.Vector.TextView.Subtitle"
|
style="@style/Widget.Vector.TextView.Subtitle.Medium"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
@ -115,7 +115,7 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/useCaseOptionThree"
|
android:id="@+id/useCaseOptionThree"
|
||||||
style="@style/Widget.Vector.TextView.Subtitle"
|
style="@style/Widget.Vector.TextView.Subtitle.Medium"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
@ -133,10 +133,12 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/useCaseSkip"
|
android:id="@+id/useCaseSkip"
|
||||||
|
style="@style/Widget.Vector.TextView.Body"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
|
android:textColor="?vctr_content_secondary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/contentFooterSpacing"
|
app:layout_constraintBottom_toTopOf="@id/contentFooterSpacing"
|
||||||
app:layout_constraintEnd_toEndOf="@id/useCaseGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/useCaseGutterEnd"
|
||||||
app:layout_constraintStart_toStartOf="@id/useCaseGutterStart"
|
app:layout_constraintStart_toStartOf="@id/useCaseGutterStart"
|
||||||
@ -153,10 +155,12 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/useCaseFooter"
|
android:id="@+id/useCaseFooter"
|
||||||
|
style="@style/Widget.Vector.TextView.Subtitle"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/ftue_auth_use_case_join_existing_server"
|
android:text="@string/ftue_auth_use_case_join_existing_server"
|
||||||
|
android:textColor="?vctr_content_secondary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/useCaseConnectToServer"
|
app:layout_constraintBottom_toTopOf="@id/useCaseConnectToServer"
|
||||||
app:layout_constraintEnd_toEndOf="@id/useCaseGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/useCaseGutterEnd"
|
||||||
app:layout_constraintStart_toStartOf="@id/useCaseGutterStart"
|
app:layout_constraintStart_toStartOf="@id/useCaseGutterStart"
|
||||||
|
@ -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"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user