mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-22 17:35:54 +03:00
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
117 changed files with 1695 additions and 552 deletions
|
@ -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)
|
||||
=======================================
|
||||
|
||||
|
|
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">
|
||||
<!-- Tint color is provided by the theme -->
|
||||
<solid android:color="@android:color/black" />
|
||||
<size
|
||||
android:width="240dp"
|
||||
android:height="44dp" />
|
||||
<corners
|
||||
android:bottomLeftRadius="12dp"
|
||||
android:bottomRightRadius="12dp"
|
|
@ -2,16 +2,14 @@
|
|||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:id="@android:id/background">
|
||||
<shape>
|
||||
<corners android:radius="8dp" />
|
||||
<solid android:color="?vctr_room_active_widgets_banner_bg" />
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="?vctr_system" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape>
|
||||
<corners android:radius="8dp" />
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/vctr_notice_secondary_alpha12" />
|
||||
</shape>
|
||||
</clip>
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<bool name="is_rtl">true</bool>
|
||||
|
||||
</resources>
|
|
@ -4,6 +4,4 @@
|
|||
<!-- Created to detect what has to be implemented (especially in the settings) -->
|
||||
<bool name="false_not_implemented">false</bool>
|
||||
|
||||
<bool name="is_rtl">false</bool>
|
||||
|
||||
</resources>
|
|
@ -137,4 +137,5 @@
|
|||
<attr name="vctr_presence_indicator_offline" format="color" />
|
||||
<color name="vctr_presence_indicator_offline_light">@color/palette_gray_100</color>
|
||||
<color name="vctr_presence_indicator_offline_dark">@color/palette_gray_450</color>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- Timeline bubble background colors -->
|
||||
<attr name="vctr_message_bubble_inbound" format="color" />
|
||||
<color name="vctr_message_bubble_inbound_light">#E8EDF4</color>
|
||||
<color name="vctr_message_bubble_inbound_dark">#21262C</color>
|
||||
<attr name="vctr_message_bubble_outbound" format="color" />
|
||||
<color name="vctr_message_bubble_outbound_light">#E7F8F3</color>
|
||||
<color name="vctr_message_bubble_outbound_dark">#133A34</color>
|
||||
</resources>
|
|
@ -15,6 +15,8 @@
|
|||
<dimen name="item_decoration_left_margin">72dp</dimen>
|
||||
<dimen name="item_event_message_state_size">16dp</dimen>
|
||||
|
||||
<dimen name="item_event_message_media_button_size">32dp</dimen>
|
||||
|
||||
<dimen name="chat_avatar_size">40dp</dimen>
|
||||
<dimen name="member_list_avatar_size">60dp</dimen>
|
||||
|
||||
|
@ -42,6 +44,7 @@
|
|||
|
||||
<!-- Preview Url -->
|
||||
<dimen name="preview_url_view_corner_radius">8dp</dimen>
|
||||
<dimen name="preview_url_view_image_max_height">160dp</dimen>
|
||||
|
||||
<dimen name="menu_item_icon_size">24dp</dimen>
|
||||
<dimen name="menu_item_size">48dp</dimen>
|
||||
|
@ -52,6 +55,12 @@
|
|||
<dimen name="composer_attachment_size">52dp</dimen>
|
||||
<dimen name="composer_attachment_margin">1dp</dimen>
|
||||
|
||||
|
||||
<dimen name="chat_bubble_margin_start">28dp</dimen>
|
||||
<dimen name="chat_bubble_margin_end">62dp</dimen>
|
||||
<dimen name="chat_bubble_fixed_size">300dp</dimen>
|
||||
<dimen name="chat_bubble_corner_radius">12dp</dimen>
|
||||
|
||||
<!-- Onboarding -->
|
||||
<item name="ftue_auth_gutter_start_percent" format="float" type="dimen">0.05</item>
|
||||
<item name="ftue_auth_gutter_end_percent" format="float" type="dimen">0.95</item>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<declare-styleable name="MessageBubble">
|
||||
<attr name="incoming_style" format="boolean" />
|
||||
<attr name="show_time_overlay" format="boolean" />
|
||||
<attr name="is_first" format="boolean" />
|
||||
<attr name="is_last" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
|
@ -6,6 +6,7 @@
|
|||
<style name="Widget.Vector.ProgressBar.Horizontal.File">
|
||||
<item name="android:indeterminateOnly">false</item>
|
||||
<item name="android:progressDrawable">@drawable/file_progress_bar</item>
|
||||
<item name="android:progressBackgroundTint">?android:colorBackground</item>
|
||||
<item name="android:minHeight">10dp</item>
|
||||
<item name="android:maxHeight">40dp</item>
|
||||
</style>
|
||||
|
|
|
@ -4,12 +4,23 @@
|
|||
<style name="TimelineContentStubBaseParams">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_marginStart">8dp</item>
|
||||
<item name="android:layout_marginLeft">8dp</item>
|
||||
<item name="android:layout_marginEnd">8dp</item>
|
||||
<item name="android:layout_marginRight">8dp</item>
|
||||
<item name="android:layout_marginBottom">4dp</item>
|
||||
<item name="android:layout_marginTop">4dp</item>
|
||||
</style>
|
||||
|
||||
<style name="TimelineContentStubContainerParams">
|
||||
<item name="android:paddingStart">8dp</item>
|
||||
<item name="android:paddingEnd">8dp</item>
|
||||
<item name="android:paddingTop">4dp</item>
|
||||
<item name="android:paddingBottom">4dp</item>
|
||||
</style>
|
||||
|
||||
<style name="TimelineContentMediaPillStyle">
|
||||
<item name="android:paddingStart">8dp</item>
|
||||
<item name="android:paddingEnd">8dp</item>
|
||||
<item name="android:paddingTop">6dp</item>
|
||||
<item name="android:paddingBottom">6dp</item>
|
||||
<item name="minHeight">48dp</item>
|
||||
<item name="android:background">@drawable/bg_media_pill</item>
|
||||
<item name="android:backgroundTint">?vctr_content_quinary</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -31,6 +31,8 @@
|
|||
<item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_dark</item>
|
||||
<item name="vctr_chat_effect_snow_background">@color/vctr_chat_effect_snow_background_dark</item>
|
||||
<item name="vctr_toolbar_background">@color/element_system_dark</item>
|
||||
<item name="vctr_message_bubble_inbound">@color/vctr_message_bubble_inbound_dark</item>
|
||||
<item name="vctr_message_bubble_outbound">@color/vctr_message_bubble_outbound_dark</item>
|
||||
|
||||
<!-- room message colors -->
|
||||
<item name="vctr_notice_secondary">#61708B</item>
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
<item name="vctr_waiting_background_color">@color/vctr_waiting_background_color_light</item>
|
||||
<item name="vctr_chat_effect_snow_background">@color/vctr_chat_effect_snow_background_light</item>
|
||||
<item name="vctr_toolbar_background">@color/element_background_light</item>
|
||||
<item name="vctr_message_bubble_inbound">@color/vctr_message_bubble_inbound_light</item>
|
||||
<item name="vctr_message_bubble_outbound">@color/vctr_message_bubble_outbound_light</item>
|
||||
|
||||
<!-- room message colors -->
|
||||
<item name="vctr_notice_secondary">#61708B</item>
|
||||
|
|
|
@ -31,7 +31,7 @@ android {
|
|||
// that the app's state is completely cleared between tests.
|
||||
testInstrumentationRunnerArguments clearPackageData: 'true'
|
||||
|
||||
buildConfigField "String", "SDK_VERSION", "\"1.3.18\""
|
||||
buildConfigField "String", "SDK_VERSION", "\"1.3.19\""
|
||||
|
||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
||||
|
|
|
@ -47,5 +47,9 @@ data class PreviewUrlData(
|
|||
// Value of field "og:description"
|
||||
val description: String?,
|
||||
// Value of field "og:image"
|
||||
val mxcUrl: String?
|
||||
val mxcUrl: String?,
|
||||
// Value of field "og:image:width"
|
||||
val imageWidth: Int?,
|
||||
// Value of field "og:image:height"
|
||||
val imageHeight: Int?
|
||||
)
|
||||
|
|
|
@ -64,4 +64,12 @@ data class MessageLocationContent(
|
|||
) : MessageContent {
|
||||
|
||||
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 {
|
||||
|
||||
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 <= 20) migrateTo21(realm)
|
||||
if (oldVersion <= 21) migrateTo22(realm)
|
||||
if (oldVersion <= 22) migrateTo23(realm)
|
||||
if (oldVersion <= 23) migrateTo24(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
|
@ -450,6 +452,22 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||
|
||||
private fun migrateTo22(realm: DynamicRealm) {
|
||||
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
|
||||
|
||||
realm.schema.get("EventEntity")
|
||||
|
@ -462,4 +480,13 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||
}
|
||||
?.addRealmObjectField(EventEntityFields.THREAD_SUMMARY_LATEST_MESSAGE.`$`, eventEntity)
|
||||
}
|
||||
|
||||
private fun migrateTo24(realm: DynamicRealm) {
|
||||
Timber.d("Step 23 -> 24")
|
||||
realm.schema.get("PreviewUrlCacheEntity")
|
||||
?.addField(PreviewUrlCacheEntityFields.IMAGE_WIDTH, Int::class.java)
|
||||
?.setNullable(PreviewUrlCacheEntityFields.IMAGE_WIDTH, true)
|
||||
?.addField(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, Int::class.java)
|
||||
?.setNullable(PreviewUrlCacheEntityFields.IMAGE_HEIGHT, true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,9 @@ internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRo
|
|||
if (deleteStateEvents) {
|
||||
stateEvents.deleteAllFromRealm()
|
||||
}
|
||||
timelineEvents.clearWith { it.deleteOnCascade(canDeleteRoot) }
|
||||
timelineEvents.clearWith {
|
||||
val deleteRoot = canDeleteRoot && (it.root?.stateKey == null || deleteStateEvents)
|
||||
it.deleteOnCascade(deleteRoot)
|
||||
}
|
||||
deleteFromRealm()
|
||||
}
|
||||
|
|
|
@ -28,7 +28,8 @@ internal open class PreviewUrlCacheEntity(
|
|||
var title: String? = null,
|
||||
var description: String? = null,
|
||||
var mxcUrl: String? = null,
|
||||
|
||||
var imageWidth: Int? = null,
|
||||
var imageHeight: Int? = null,
|
||||
var lastUpdatedTimestamp: Long = 0L
|
||||
) : RealmObject() {
|
||||
|
||||
|
|
|
@ -48,8 +48,8 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
|
|||
|
||||
override suspend fun execute(params: GetPreviewUrlTask.Params): PreviewUrlData {
|
||||
return when (params.cacheStrategy) {
|
||||
CacheStrategy.NoCache -> doRequest(params.url, params.timestamp)
|
||||
is CacheStrategy.TtlCache -> doRequestWithCache(
|
||||
CacheStrategy.NoCache -> doRequest(params.url, params.timestamp)
|
||||
is CacheStrategy.TtlCache -> doRequestWithCache(
|
||||
params.url,
|
||||
params.timestamp,
|
||||
params.cacheStrategy.validityDurationInMillis,
|
||||
|
@ -77,7 +77,9 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
|
|||
siteName = (get("og:site_name") as? String)?.unescapeHtml(),
|
||||
title = (get("og:title") as? String)?.unescapeHtml(),
|
||||
description = (get("og:description") as? String)?.unescapeHtml(),
|
||||
mxcUrl = get("og:image") as? String
|
||||
mxcUrl = get("og:image") as? String,
|
||||
imageHeight = (get("og:image:height") as? Double)?.toInt(),
|
||||
imageWidth = (get("og:image:width") as? Double)?.toInt(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -114,7 +116,8 @@ internal class DefaultGetPreviewUrlTask @Inject constructor(
|
|||
previewUrlCacheEntity.title = data.title
|
||||
previewUrlCacheEntity.description = data.description
|
||||
previewUrlCacheEntity.mxcUrl = data.mxcUrl
|
||||
|
||||
previewUrlCacheEntity.imageHeight = data.imageHeight
|
||||
previewUrlCacheEntity.imageWidth = data.imageWidth
|
||||
previewUrlCacheEntity.lastUpdatedTimestamp = Date().time
|
||||
}
|
||||
|
||||
|
|
|
@ -27,5 +27,7 @@ internal fun PreviewUrlCacheEntity.toDomain() = PreviewUrlData(
|
|||
siteName = siteName,
|
||||
title = title,
|
||||
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 {
|
||||
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
|
||||
if (isLimited && lastChunk != null) {
|
||||
lastChunk.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true)
|
||||
lastChunk.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = true)
|
||||
}
|
||||
val chunkEntity = if (!isLimited && lastChunk != null) {
|
||||
lastChunk
|
||||
|
|
|
@ -18,7 +18,7 @@ ext.versionMinor = 3
|
|||
// 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
|
||||
// is the value for the next regular release.
|
||||
ext.versionPatch = 18
|
||||
ext.versionPatch = 19
|
||||
|
||||
static def getGitTimestamp() {
|
||||
def cmd = 'git show -s --format=%ct'
|
||||
|
|
|
@ -40,8 +40,11 @@ class OnboardingRobot {
|
|||
|
||||
private fun crawlGetStarted() {
|
||||
clickOn(R.id.loginSplashSubmit)
|
||||
assertDisplayed(R.id.useCaseHeaderTitle, R.string.ftue_auth_use_case_title)
|
||||
clickOn(R.id.useCaseOptionOne)
|
||||
OnboardingServersRobot().crawlSignUp()
|
||||
pressBack()
|
||||
pressBack()
|
||||
}
|
||||
|
||||
private fun crawlAlreadyHaveAccount() {
|
||||
|
@ -66,6 +69,7 @@ class OnboardingRobot {
|
|||
assertDisplayed(R.id.loginSplashSubmit, R.string.login_splash_create_account)
|
||||
if (createAccount) {
|
||||
clickOn(R.id.loginSplashSubmit)
|
||||
clickOn(R.id.useCaseOptionOne)
|
||||
} else {
|
||||
clickOn(R.id.loginSplashAlreadyHaveAccount)
|
||||
}
|
||||
|
|
|
@ -76,6 +76,9 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
|
|||
@EpoxyAttribute
|
||||
var locationPinProvider: LocationPinProvider? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var locationOwnerId: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var movementMethod: MovementMethod? = null
|
||||
|
||||
|
@ -109,7 +112,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
|
|||
.apply(RequestOptions.centerCropTransform())
|
||||
.into(holder.staticMapImageView)
|
||||
|
||||
locationPinProvider?.create(matrixItem.id) { pinDrawable ->
|
||||
locationPinProvider?.create(locationOwnerId) { pinDrawable ->
|
||||
GlideApp.with(holder.staticMapPinImageView)
|
||||
.load(pinDrawable)
|
||||
.into(holder.staticMapPinImageView)
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
package im.vector.app.core.resources
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
@ -29,3 +31,7 @@ class LocaleProvider @Inject constructor(private val resources: Resources) {
|
|||
}
|
||||
|
||||
fun LocaleProvider.isEnglishSpeaking() = current().language.startsWith("en")
|
||||
|
||||
fun LocaleProvider.getLayoutDirectionFromCurrentLocale() = TextUtils.getLayoutDirectionFromLocale(current())
|
||||
|
||||
fun LocaleProvider.isRTL() = getLayoutDirectionFromCurrentLocale() == View.LAYOUT_DIRECTION_RTL
|
||||
|
|
|
@ -36,5 +36,5 @@ class DefaultVectorFeatures : VectorFeatures {
|
|||
override fun onboardingVariant(): VectorFeatures.OnboardingVariant = BuildConfig.ONBOARDING_VARIANT
|
||||
override fun isOnboardingAlreadyHaveAccountSplashEnabled() = 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) {
|
||||
val isSelfLocation = locationContent.isSelfLocation()
|
||||
navigator
|
||||
.openLocationSharing(
|
||||
context = requireContext(),
|
||||
roomId = timelineArgs.roomId,
|
||||
mode = LocationSharingMode.PREVIEW,
|
||||
initialLocationData = locationContent.toLocationData(),
|
||||
locationOwnerId = senderId
|
||||
locationOwnerId = if (isSelfLocation) senderId else null
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -382,20 +382,28 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
(0 until modelCache.size).forEach { position ->
|
||||
val event = currentSnapshot[position]
|
||||
val nextEvent = currentSnapshot.nextOrNull(position)
|
||||
val prevEvent = currentSnapshot.prevOrNull(position)
|
||||
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
|
||||
timelineEventVisibilityHelper.shouldShowEvent(
|
||||
timelineEvent = it,
|
||||
highlightedEventId = partialState.highlightedEventId,
|
||||
isFromThreadTimeline = partialState.isFromThreadTimeline(),
|
||||
rootThreadEventId = partialState.rootThreadEventId)
|
||||
}
|
||||
// Should be build if not cached or if model should be refreshed
|
||||
if (modelCache[position] == null || modelCache[position]?.isCacheable(partialState) == false) {
|
||||
val prevEvent = currentSnapshot.prevOrNull(position)
|
||||
val prevDisplayableEvent = currentSnapshot.subList(0, position).lastOrNull {
|
||||
timelineEventVisibilityHelper.shouldShowEvent(
|
||||
timelineEvent = it,
|
||||
highlightedEventId = partialState.highlightedEventId,
|
||||
isFromThreadTimeline = partialState.isFromThreadTimeline(),
|
||||
rootThreadEventId = partialState.rootThreadEventId)
|
||||
}
|
||||
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
|
||||
timelineEventVisibilityHelper.shouldShowEvent(
|
||||
timelineEvent = it,
|
||||
highlightedEventId = partialState.highlightedEventId,
|
||||
isFromThreadTimeline = partialState.isFromThreadTimeline(),
|
||||
rootThreadEventId = partialState.rootThreadEventId)
|
||||
}
|
||||
val timelineEventsGroup = timelineEventsGroups.getOrNull(event)
|
||||
val params = TimelineItemFactoryParams(
|
||||
event = event,
|
||||
prevEvent = prevEvent,
|
||||
prevDisplayableEvent = prevDisplayableEvent,
|
||||
nextEvent = nextEvent,
|
||||
nextDisplayableEvent = nextDisplayableEvent,
|
||||
partialState = partialState,
|
||||
|
|
|
@ -45,6 +45,7 @@ import im.vector.app.features.location.toLocationData
|
|||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
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.session.events.model.toModel
|
||||
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 body = state.messageBody.linkify(host.listener)
|
||||
val bindingOptions = spanUtils.getBindingOptions(body)
|
||||
val locationUrl = state.timelineEvent()?.root?.getClearContent()
|
||||
|
||||
val locationContent = state.timelineEvent()?.root?.getClearContent()
|
||||
?.toModel<MessageLocationContent>(catchError = true)
|
||||
?.toLocationData()
|
||||
val locationUrl = locationContent?.toLocationData()
|
||||
?.let { urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, 1200, 800) }
|
||||
val locationOwnerId = if (locationContent?.isSelfLocation().orTrue()) state.informationData.matrixItem.id else null
|
||||
|
||||
bottomSheetMessagePreviewItem {
|
||||
id("preview")
|
||||
|
@ -96,6 +99,7 @@ class MessageActionsEpoxyController @Inject constructor(
|
|||
time(formattedDate)
|
||||
locationUrl(locationUrl)
|
||||
locationPinProvider(host.locationPinProvider)
|
||||
locationOwnerId(locationOwnerId)
|
||||
}
|
||||
|
||||
// Send state
|
||||
|
|
|
@ -113,6 +113,7 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat
|
|||
callback = params.callback,
|
||||
threadDetails = threadDetails)
|
||||
return MessageTextItem_()
|
||||
.layout(informationData.messageLayout.layoutRes)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.highlighted(params.isHighlighted)
|
||||
.attributes(attributes)
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.timeline.factory
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
|
@ -44,8 +43,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttrib
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageImageVideoItem
|
||||
|
@ -66,7 +63,6 @@ import im.vector.app.features.home.room.detail.timeline.item.VerificationRequest
|
|||
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||
import im.vector.app.features.html.CodeVisitor
|
||||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
import im.vector.app.features.html.PillsPostProcessor
|
||||
import im.vector.app.features.html.SpanUtils
|
||||
|
@ -79,7 +75,6 @@ import im.vector.app.features.media.VideoContentRenderer
|
|||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
import me.gujun.android.span.span
|
||||
import org.commonmark.node.Document
|
||||
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -134,7 +129,6 @@ class MessageItemFactory @Inject constructor(
|
|||
private val locationPinProvider: LocationPinProvider,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val urlMapProvider: UrlMapProvider,
|
||||
private val resources: Resources
|
||||
) {
|
||||
|
||||
// TODO inject this properly?
|
||||
|
@ -181,7 +175,7 @@ class MessageItemFactory @Inject constructor(
|
|||
|
||||
// val all = event.root.toContent()
|
||||
// val ev = all.toModel<Event>()
|
||||
return when (messageContent) {
|
||||
val messageItem = when (messageContent) {
|
||||
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
|
@ -206,23 +200,30 @@ class MessageItemFactory @Inject constructor(
|
|||
}
|
||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
}
|
||||
return messageItem?.apply {
|
||||
layout(informationData.messageLayout.layoutRes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildLocationItem(locationContent: MessageLocationContent,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
attributes: AbsMessageItem.Attributes): MessageLocationItem? {
|
||||
val width = resources.displayMetrics.widthPixels - dimensionConverter.dpToPx(60)
|
||||
val width = timelineMediaSizeProvider.getMaxSize().first
|
||||
val height = dimensionConverter.dpToPx(200)
|
||||
|
||||
val locationUrl = locationContent.toLocationData()?.let {
|
||||
urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height)
|
||||
}
|
||||
|
||||
val userId = if (locationContent.isSelfLocation()) informationData.senderId else null
|
||||
|
||||
return MessageLocationItem_()
|
||||
.attributes(attributes)
|
||||
.locationUrl(locationUrl)
|
||||
.userId(informationData.senderId)
|
||||
.mapWidth(width)
|
||||
.mapHeight(height)
|
||||
.userId(userId)
|
||||
.locationPinProvider(locationPinProvider)
|
||||
.highlighted(highlight)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
|
@ -524,46 +525,22 @@ class MessageItemFactory @Inject constructor(
|
|||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? {
|
||||
val isFormatted = messageContent.matrixFormattedBody.isNullOrBlank().not()
|
||||
return if (isFormatted) {
|
||||
// First detect if the message contains some code block(s) or inline code
|
||||
val localFormattedBody = htmlRenderer.get().parse(messageContent.body) as Document
|
||||
val codeVisitor = CodeVisitor()
|
||||
codeVisitor.visit(localFormattedBody)
|
||||
when (codeVisitor.codeKind) {
|
||||
CodeVisitor.Kind.BLOCK -> {
|
||||
val codeFormattedBlock = htmlRenderer.get().render(localFormattedBody)
|
||||
if (codeFormattedBlock == null) {
|
||||
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
||||
} else {
|
||||
buildCodeBlockItem(codeFormattedBlock, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
CodeVisitor.Kind.INLINE -> {
|
||||
val codeFormatted = htmlRenderer.get().render(localFormattedBody)
|
||||
if (codeFormatted == null) {
|
||||
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
||||
} else {
|
||||
buildMessageTextItem(codeFormatted, false, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
CodeVisitor.Kind.NONE -> {
|
||||
buildFormattedTextItem(messageContent, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
val matrixFormattedBody = messageContent.matrixFormattedBody
|
||||
return if (matrixFormattedBody != null) {
|
||||
buildFormattedTextItem(matrixFormattedBody, informationData, highlight, callback, attributes)
|
||||
} else {
|
||||
buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildFormattedTextItem(messageContent: MessageTextContent,
|
||||
private fun buildFormattedTextItem(matrixFormattedBody: String,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes): MessageTextItem? {
|
||||
val compressed = htmlCompressor.compress(messageContent.formattedBody!!)
|
||||
val formattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor)
|
||||
return buildMessageTextItem(formattedBody, true, informationData, highlight, callback, attributes)
|
||||
val compressed = htmlCompressor.compress(matrixFormattedBody)
|
||||
val renderedFormattedBody = htmlRenderer.get().render(compressed, pillsPostProcessor) as Spanned
|
||||
return buildMessageTextItem(renderedFormattedBody, true, informationData, highlight, callback, attributes)
|
||||
}
|
||||
|
||||
private fun buildMessageTextItem(body: CharSequence,
|
||||
|
@ -596,24 +573,6 @@ class MessageItemFactory @Inject constructor(
|
|||
.movementMethod(createLinkMovementMethod(callback))
|
||||
}
|
||||
|
||||
private fun buildCodeBlockItem(formattedBody: CharSequence,
|
||||
informationData: MessageInformationData,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes): MessageBlockCodeItem? {
|
||||
return MessageBlockCodeItem_()
|
||||
.apply {
|
||||
if (informationData.hasBeenEdited) {
|
||||
val spannable = annotateWithEdited("", callback, informationData)
|
||||
editedSpan(spannable.toEpoxyCharSequence())
|
||||
}
|
||||
}
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.attributes(attributes)
|
||||
.highlighted(highlight)
|
||||
.message(formattedBody.toEpoxyCharSequence())
|
||||
}
|
||||
|
||||
private fun annotateWithEdited(linkifiedBody: CharSequence,
|
||||
callback: TimelineEventController.Callback?,
|
||||
informationData: MessageInformationData): Spannable {
|
||||
|
@ -719,6 +678,7 @@ class MessageItemFactory @Inject constructor(
|
|||
private fun buildRedactedItem(attributes: AbsMessageItem.Attributes,
|
||||
highlight: Boolean): RedactedMessageItem? {
|
||||
return RedactedMessageItem_()
|
||||
.layout(attributes.informationData.messageLayout.layoutRes)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.attributes(attributes)
|
||||
.highlighted(highlight)
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
|||
data class TimelineItemFactoryParams(
|
||||
val event: TimelineEvent,
|
||||
val prevEvent: TimelineEvent? = null,
|
||||
val prevDisplayableEvent: TimelineEvent? = null,
|
||||
val nextEvent: TimelineEvent? = null,
|
||||
val nextDisplayableEvent: TimelineEvent? = null,
|
||||
val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(),
|
||||
|
|
|
@ -17,14 +17,22 @@
|
|||
package im.vector.app.features.home.room.detail.timeline.helper
|
||||
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineLayoutSettings
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineLayoutSettingsProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
class AvatarSizeProvider @Inject constructor(private val dimensionConverter: DimensionConverter) {
|
||||
class AvatarSizeProvider @Inject constructor(private val dimensionConverter: DimensionConverter,
|
||||
private val layoutSettingsProvider: TimelineLayoutSettingsProvider) {
|
||||
|
||||
private val avatarStyle = AvatarStyle.SMALL
|
||||
private val avatarStyle by lazy {
|
||||
when (layoutSettingsProvider.getLayoutSettings()) {
|
||||
TimelineLayoutSettings.MODERN -> AvatarStyle.SMALL
|
||||
TimelineLayoutSettings.BUBBLE -> AvatarStyle.BUBBLE
|
||||
}
|
||||
}
|
||||
|
||||
val leftGuideline: Int by lazy {
|
||||
dimensionConverter.dpToPx(avatarStyle.avatarSizeDP + 8)
|
||||
dimensionConverter.dpToPx(avatarStyle.avatarSizeDP + avatarStyle.marginDP)
|
||||
}
|
||||
|
||||
val avatarSize: Int by lazy {
|
||||
|
@ -33,11 +41,12 @@ class AvatarSizeProvider @Inject constructor(private val dimensionConverter: Dim
|
|||
|
||||
companion object {
|
||||
|
||||
enum class AvatarStyle(val avatarSizeDP: Int) {
|
||||
BIG(50),
|
||||
MEDIUM(40),
|
||||
SMALL(30),
|
||||
NONE(0)
|
||||
enum class AvatarStyle(val avatarSizeDP: Int, val marginDP: Int) {
|
||||
BIG(50, 8),
|
||||
MEDIUM(40, 8),
|
||||
SMALL(30, 8),
|
||||
BUBBLE(28, 4),
|
||||
NONE(0, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,16 +22,12 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
|
|||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageFileItem
|
||||
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
|
||||
import javax.inject.Inject
|
||||
|
||||
@ActivityScoped
|
||||
class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val messageColorProvider: MessageColorProvider,
|
||||
private val errorFormatter: ErrorFormatter) {
|
||||
class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder) {
|
||||
|
||||
private val updateListeners = mutableMapOf<String, ContentDownloadUpdater>()
|
||||
|
||||
|
@ -39,7 +35,7 @@ class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSe
|
|||
holder: MessageFileItem.Holder) {
|
||||
activeSessionHolder.getSafeActiveSession()?.also { session ->
|
||||
val downloadStateTracker = session.contentDownloadProgressTracker()
|
||||
val updateListener = ContentDownloadUpdater(holder, messageColorProvider, errorFormatter)
|
||||
val updateListener = ContentDownloadUpdater(holder)
|
||||
updateListeners[mxcUrl] = updateListener
|
||||
downloadStateTracker.track(mxcUrl, updateListener)
|
||||
}
|
||||
|
@ -62,9 +58,7 @@ class ContentDownloadStateTrackerBinder @Inject constructor(private val activeSe
|
|||
}
|
||||
}
|
||||
|
||||
private class ContentDownloadUpdater(private val holder: MessageFileItem.Holder,
|
||||
private val messageColorProvider: MessageColorProvider,
|
||||
private val errorFormatter: ErrorFormatter) : ContentDownloadStateTracker.UpdateListener {
|
||||
private class ContentDownloadUpdater(private val holder: MessageFileItem.Holder) : ContentDownloadStateTracker.UpdateListener {
|
||||
|
||||
override fun onDownloadStateUpdate(state: ContentDownloadStateTracker.State) {
|
||||
when (state) {
|
||||
|
@ -124,7 +118,7 @@ private class ContentDownloadUpdater(private val holder: MessageFileItem.Holder,
|
|||
private fun handleSuccess() {
|
||||
stop()
|
||||
holder.fileDownloadProgress.isIndeterminate = false
|
||||
holder.fileDownloadProgress.progress = 100
|
||||
holder.fileDownloadProgress.progress = 0
|
||||
holder.fileImageView.setImageResource(R.drawable.ic_paperclip)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,17 @@ class LocationPinProvider @Inject constructor(
|
|||
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)) {
|
||||
callback(cache[userId]!!)
|
||||
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.ReferencesInfoData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayoutFactory
|
||||
import org.matrix.android.sdk.api.crypto.VerificationState
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState
|
|||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.hasBeenEdited
|
||||
import org.matrix.android.sdk.api.session.room.timeline.isEdition
|
||||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -51,35 +50,28 @@ import javax.inject.Inject
|
|||
*/
|
||||
class MessageInformationDataFactory @Inject constructor(private val session: Session,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
private val visibilityHelper: TimelineEventVisibilityHelper,
|
||||
private val vectorPreferences: VectorPreferences) {
|
||||
private val messageLayoutFactory: TimelineMessageLayoutFactory) {
|
||||
|
||||
fun create(params: TimelineItemFactoryParams): MessageInformationData {
|
||||
val event = params.event
|
||||
val nextDisplayableEvent = params.nextDisplayableEvent
|
||||
val prevDisplayableEvent = params.prevDisplayableEvent
|
||||
val eventId = event.eventId
|
||||
val isSentByMe = event.root.senderId == session.myUserId
|
||||
val roomSummary = params.partialState.roomSummary
|
||||
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextDisplayableEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
||||
?: false
|
||||
|
||||
val showInformation =
|
||||
addDaySeparator ||
|
||||
event.senderInfo.avatarUrl != nextDisplayableEvent?.senderInfo?.avatarUrl ||
|
||||
event.senderInfo.disambiguatedDisplayName != nextDisplayableEvent?.senderInfo?.disambiguatedDisplayName ||
|
||||
nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) ||
|
||||
isNextMessageReceivedMoreThanOneHourAgo ||
|
||||
isTileTypeMessage(nextDisplayableEvent) ||
|
||||
nextDisplayableEvent.isEdition()
|
||||
val isFirstFromThisSender = nextDisplayableEvent?.root?.senderId != event.root.senderId || addDaySeparator
|
||||
val isLastFromThisSender = prevDisplayableEvent?.root?.senderId != event.root.senderId ||
|
||||
prevDisplayableEvent?.root?.localDateTime()?.toLocalDate() != date.toLocalDate()
|
||||
|
||||
val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
|
||||
val roomSummary = params.partialState.roomSummary
|
||||
val e2eDecoration = getE2EDecoration(roomSummary, event)
|
||||
|
||||
// SendState Decoration
|
||||
val isSentByMe = event.root.senderId == session.myUserId
|
||||
val sendStateDecoration = if (isSentByMe) {
|
||||
getSendStateDecoration(
|
||||
event = event,
|
||||
|
@ -90,6 +82,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||
SendStateDecoration.NONE
|
||||
}
|
||||
|
||||
val messageLayout = messageLayoutFactory.create(params)
|
||||
|
||||
return MessageInformationData(
|
||||
eventId = eventId,
|
||||
senderId = event.root.senderId ?: "",
|
||||
|
@ -98,8 +92,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||
ageLocalTS = event.root.ageLocalTs,
|
||||
avatarUrl = event.senderInfo.avatarUrl,
|
||||
memberName = event.senderInfo.disambiguatedDisplayName,
|
||||
showInformation = showInformation,
|
||||
forceShowTimestamp = vectorPreferences.alwaysShowTimeStamps(),
|
||||
messageLayout = messageLayout,
|
||||
orderedReactionList = event.annotations?.reactionsSummary
|
||||
// ?.filter { isSingleEmoji(it.key) }
|
||||
?.map {
|
||||
|
@ -127,6 +120,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||
ReferencesInfoData(verificationState)
|
||||
},
|
||||
sentByMe = isSentByMe,
|
||||
isFirstFromThisSender = isFirstFromThisSender,
|
||||
isLastFromThisSender = isLastFromThisSender,
|
||||
e2eDecoration = e2eDecoration,
|
||||
sendStateDecoration = sendStateDecoration
|
||||
)
|
||||
|
|
|
@ -16,13 +16,17 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.timeline.helper
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
import im.vector.app.R
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@ActivityScoped
|
||||
class TimelineMediaSizeProvider @Inject constructor() {
|
||||
class TimelineMediaSizeProvider @Inject constructor(private val resources: Resources,
|
||||
private val vectorPreferences: VectorPreferences) {
|
||||
|
||||
var recyclerView: RecyclerView? = null
|
||||
private var cachedSize: Pair<Int, Int>? = null
|
||||
|
@ -41,9 +45,14 @@ class TimelineMediaSizeProvider @Inject constructor() {
|
|||
maxImageWidth = (width * 0.7f).roundToInt()
|
||||
maxImageHeight = (height * 0.5f).roundToInt()
|
||||
} else {
|
||||
maxImageWidth = (width * 0.5f).roundToInt()
|
||||
maxImageWidth = (width * 0.7f).roundToInt()
|
||||
maxImageHeight = (height * 0.7f).roundToInt()
|
||||
}
|
||||
return Pair(maxImageWidth, maxImageHeight)
|
||||
return if (vectorPreferences.useMessageBubblesLayout()) {
|
||||
val bubbleMaxImageWidth = maxImageWidth.coerceAtMost(resources.getDimensionPixelSize(R.dimen.chat_bubble_fixed_size))
|
||||
Pair(bubbleMaxImageWidth, maxImageHeight)
|
||||
} else {
|
||||
Pair(maxImageWidth, maxImageHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import im.vector.app.core.ui.views.ShieldImageView
|
|||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
|
||||
import im.vector.app.features.reactions.widget.ReactionButton
|
||||
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
|
@ -98,6 +99,7 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
|
|||
|
||||
holder.view.onClick(baseAttributes.itemClickListener)
|
||||
holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener)
|
||||
(holder.view as? TimelineMessageLayoutRenderer)?.renderMessageLayout(baseAttributes.informationData.messageLayout)
|
||||
}
|
||||
|
||||
override fun unbind(holder: H) {
|
||||
|
|
|
@ -25,7 +25,6 @@ import android.widget.RelativeLayout
|
|||
import android.widget.TextView
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
|
@ -75,38 +74,37 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
|||
|
||||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
if (attributes.informationData.showInformation) {
|
||||
if (attributes.informationData.messageLayout.showAvatar) {
|
||||
holder.avatarImageView.layoutParams = holder.avatarImageView.layoutParams?.apply {
|
||||
height = attributes.avatarSize
|
||||
width = attributes.avatarSize
|
||||
}
|
||||
holder.avatarImageView.visibility = View.VISIBLE
|
||||
holder.avatarImageView.onClick(_avatarClickListener)
|
||||
holder.memberNameView.visibility = View.VISIBLE
|
||||
holder.memberNameView.onClick(_memberNameClickListener)
|
||||
holder.timeView.visibility = View.VISIBLE
|
||||
holder.timeView.text = attributes.informationData.time
|
||||
holder.memberNameView.text = attributes.informationData.memberName
|
||||
holder.memberNameView.setTextColor(attributes.getMemberNameColor())
|
||||
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
|
||||
holder.avatarImageView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.avatarImageView.isVisible = true
|
||||
holder.avatarImageView.onClick(_avatarClickListener)
|
||||
} else {
|
||||
holder.avatarImageView.setOnClickListener(null)
|
||||
holder.memberNameView.setOnClickListener(null)
|
||||
holder.avatarImageView.visibility = View.GONE
|
||||
if (attributes.informationData.forceShowTimestamp) {
|
||||
holder.memberNameView.isInvisible = true
|
||||
holder.timeView.isVisible = true
|
||||
holder.timeView.text = attributes.informationData.time
|
||||
} else {
|
||||
holder.memberNameView.isVisible = false
|
||||
holder.timeView.isVisible = false
|
||||
}
|
||||
holder.avatarImageView.setOnLongClickListener(null)
|
||||
holder.memberNameView.setOnLongClickListener(null)
|
||||
holder.avatarImageView.isVisible = false
|
||||
}
|
||||
if (attributes.informationData.messageLayout.showDisplayName) {
|
||||
holder.memberNameView.isVisible = true
|
||||
holder.memberNameView.text = attributes.informationData.memberName
|
||||
holder.memberNameView.setTextColor(attributes.getMemberNameColor())
|
||||
holder.memberNameView.onClick(_memberNameClickListener)
|
||||
holder.memberNameView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
} else {
|
||||
holder.memberNameView.setOnClickListener(null)
|
||||
holder.memberNameView.setOnLongClickListener(null)
|
||||
holder.memberNameView.isVisible = false
|
||||
}
|
||||
if (attributes.informationData.messageLayout.showTimestamp) {
|
||||
holder.timeView.isVisible = true
|
||||
holder.timeView.text = attributes.informationData.time
|
||||
} else {
|
||||
holder.timeView.isVisible = false
|
||||
}
|
||||
|
||||
// Render send state indicator
|
||||
holder.sendStateImageView.render(attributes.informationData.sendStateDecoration)
|
||||
holder.eventSendingIndicator.isVisible = attributes.informationData.sendStateDecoration == SendStateDecoration.SENDING_MEDIA
|
||||
|
|
|
@ -26,7 +26,6 @@ import im.vector.app.R
|
|||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.platform.CheckableView
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
|
||||
/**
|
||||
* Children must override getViewType()
|
||||
|
@ -40,8 +39,18 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||
@EpoxyAttribute
|
||||
open var leftGuideline: Int = 0
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
lateinit var dimensionConverter: DimensionConverter
|
||||
final override fun getViewType(): Int {
|
||||
// This makes sure we have a unique integer for the combination of layout and ViewStubId.
|
||||
val pairingResult = pairingFunction(layout.toLong(), getViewStubId().toLong())
|
||||
return (pairingResult - Int.MAX_VALUE).toInt()
|
||||
}
|
||||
|
||||
abstract fun getViewStubId(): Int
|
||||
|
||||
// Szudzik function
|
||||
private fun pairingFunction(a: Long, b: Long): Long {
|
||||
return if (a >= b) a * a + a + b else a + b * b
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun bind(holder: H) {
|
||||
|
|
|
@ -50,7 +50,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
|
|||
@EpoxyAttribute
|
||||
lateinit var attributes: Attributes
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
|
|
@ -46,7 +46,7 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
|
|||
return listOf(attributes.informationData.eventId)
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
class Holder : BaseHolder(STUB_ID) {
|
||||
val avatarImageView by bind<ImageView>(R.id.itemDefaultAvatarView)
|
||||
|
|
|
@ -29,7 +29,7 @@ import im.vector.app.features.home.AvatarRenderer
|
|||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
@EpoxyAttribute
|
||||
override lateinit var attributes: Attributes
|
||||
|
|
|
@ -51,7 +51,7 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
|||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var movementMethod: MovementMethod? = null
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.widget.TextView
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
|
||||
import me.saket.bettermovementmethod.BetterLinkMovementMethod
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageBlockCodeItem : AbsMessageItem<MessageBlockCodeItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
var message: EpoxyCharSequence? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var editedSpan: EpoxyCharSequence? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.messageView.text = message?.charSequence
|
||||
renderSendState(holder.messageView, holder.messageView)
|
||||
holder.messageView.onClick(attributes.itemClickListener)
|
||||
holder.messageView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.editedView.movementMethod = BetterLinkMovementMethod.getInstance()
|
||||
holder.editedView.setTextOrHide(editedSpan?.charSequence)
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
val messageView by bind<TextView>(R.id.codeBlockTextView)
|
||||
val editedView by bind<TextView>(R.id.codeBlockEditedView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageContentCodeBlockStub
|
||||
}
|
||||
}
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
|
@ -29,6 +31,8 @@ import im.vector.app.R
|
|||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||
|
@ -73,15 +77,19 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
|||
} else {
|
||||
if (izDownloaded) {
|
||||
holder.fileImageView.setImageResource(iconRes)
|
||||
holder.fileDownloadProgress.progress = 100
|
||||
holder.fileDownloadProgress.progress = 0
|
||||
} else {
|
||||
contentDownloadStateTrackerBinder.bind(mxcUrl, holder)
|
||||
holder.fileImageView.setImageResource(R.drawable.ic_download)
|
||||
holder.fileDownloadProgress.progress = 0
|
||||
}
|
||||
}
|
||||
// holder.view.setOnClickListener(clickListener)
|
||||
|
||||
val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) {
|
||||
Color.TRANSPARENT
|
||||
} else {
|
||||
ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary)
|
||||
}
|
||||
holder.mainLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
|
||||
holder.filenameView.onClick(attributes.itemClickListener)
|
||||
holder.filenameView.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.fileImageWrapper.onClick(attributes.itemClickListener)
|
||||
|
@ -95,9 +103,10 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
|||
contentDownloadStateTrackerBinder.unbind(mxcUrl)
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
val mainLayout by bind<ViewGroup>(R.id.messageFileMainLayout)
|
||||
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
|
||||
val fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
|
||||
val fileImageView by bind<ImageView>(R.id.messageFileIconView)
|
||||
|
|
|
@ -23,12 +23,16 @@ import androidx.core.view.ViewCompat
|
|||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.epoxy.ClickListener
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.files.LocalFilesHelper
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
|
@ -54,7 +58,14 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
imageContentRenderer.render(mediaData, mode, holder.imageView)
|
||||
val messageLayout = baseAttributes.informationData.messageLayout
|
||||
val dimensionConverter = DimensionConverter(holder.view.resources)
|
||||
val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
|
||||
messageLayout.cornersRadius.granularRoundedCorners()
|
||||
} else {
|
||||
RoundedCorners(dimensionConverter.dpToPx(8))
|
||||
}
|
||||
imageContentRenderer.render(mediaData, mode, holder.imageView, imageCornerTransformation)
|
||||
if (!attributes.informationData.sendState.hasFailed()) {
|
||||
contentUploadStateTrackerBinder.bind(
|
||||
attributes.informationData.eventId,
|
||||
|
@ -81,7 +92,7 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
|
|||
super.unbind(holder)
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
val progressLayout by bind<ViewGroup>(R.id.messageMediaUploadProgressLayout)
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.os.Parcelable
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.crypto.VerificationState
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
|
@ -31,8 +32,7 @@ data class MessageInformationData(
|
|||
val ageLocalTS: Long?,
|
||||
val avatarUrl: String?,
|
||||
val memberName: CharSequence? = null,
|
||||
val showInformation: Boolean = true,
|
||||
val forceShowTimestamp: Boolean = false,
|
||||
val messageLayout: TimelineMessageLayout,
|
||||
/*List of reactions (emoji,count,isSelected)*/
|
||||
val orderedReactionList: List<ReactionInfoData>? = null,
|
||||
val pollResponseAggregatedSummary: PollResponseData? = null,
|
||||
|
@ -41,7 +41,9 @@ data class MessageInformationData(
|
|||
val referencesInfoData: ReferencesInfoData? = null,
|
||||
val sentByMe: Boolean,
|
||||
val e2eDecoration: E2EDecoration = E2EDecoration.NONE,
|
||||
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE
|
||||
val sendStateDecoration: SendStateDecoration = SendStateDecoration.NONE,
|
||||
val isFirstFromThisSender: Boolean = false,
|
||||
val isLastFromThisSender: Boolean = false
|
||||
) : Parcelable {
|
||||
|
||||
val matrixItem: MatrixItem
|
||||
|
|
|
@ -20,16 +20,21 @@ import android.graphics.drawable.Drawable
|
|||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import com.bumptech.glide.load.DataSource
|
||||
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.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() {
|
||||
|
@ -41,15 +46,29 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
|
|||
var userId: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var mapWidth: Int = 0
|
||||
|
||||
@EpoxyAttribute
|
||||
var mapHeight: Int = 0
|
||||
|
||||
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
|
||||
var locationPinProvider: LocationPinProvider? = null
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
renderSendState(holder.view, null)
|
||||
|
||||
val location = locationUrl ?: return
|
||||
val 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)
|
||||
.load(location)
|
||||
.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 {
|
||||
locationPinProvider?.create(locationOwnerId) { pinDrawable ->
|
||||
locationPinProvider?.create(userId) { pinDrawable ->
|
||||
GlideApp.with(holder.staticMapPinImageView)
|
||||
.load(pinDrawable)
|
||||
.into(holder.staticMapPinImageView)
|
||||
|
@ -70,10 +89,11 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
|
|||
return false
|
||||
}
|
||||
})
|
||||
.transform(imageCornerTransformation)
|
||||
.into(holder.staticMapImageView)
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
val staticMapImageView by bind<ImageView>(R.id.staticMapImageView)
|
||||
|
|
|
@ -80,6 +80,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||
safePreviewUrlRetriever.addListener(attributes.informationData.eventId, previewUrlViewUpdater)
|
||||
}
|
||||
holder.previewUrlView.delegate = previewUrlCallback
|
||||
holder.previewUrlView.renderMessageLayout(attributes.informationData.messageLayout)
|
||||
|
||||
if (useBigFont) {
|
||||
holder.messageView.textSize = 44F
|
||||
|
@ -121,7 +122,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
|||
previewUrlRetriever?.removeListener(attributes.informationData.eventId, previewUrlViewUpdater)
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
|
||||
|
|
|
@ -16,7 +16,10 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.text.format.DateUtils
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
|
@ -29,6 +32,8 @@ import im.vector.app.core.epoxy.ClickListener
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceMessagePlaybackTracker
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
||||
|
@ -80,6 +85,12 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
}
|
||||
}
|
||||
|
||||
val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) {
|
||||
Color.TRANSPARENT
|
||||
} else {
|
||||
ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary)
|
||||
}
|
||||
holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint)
|
||||
holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) }
|
||||
|
||||
voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : VoiceMessagePlaybackTracker.Listener {
|
||||
|
@ -120,9 +131,10 @@ abstract class MessageVoiceItem : AbsMessageItem<MessageVoiceItem.Holder>() {
|
|||
voiceMessagePlaybackTracker.unTrack(attributes.informationData.eventId)
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
val voicePlaybackLayout by bind<View>(R.id.voicePlaybackLayout)
|
||||
val voiceLayout by bind<ViewGroup>(R.id.voiceLayout)
|
||||
val voicePlaybackControlButton by bind<ImageButton>(R.id.voicePlaybackControlButton)
|
||||
val voicePlaybackTime by bind<TextView>(R.id.voicePlaybackTime)
|
||||
|
|
|
@ -64,7 +64,7 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
|||
return listOf(attributes.informationData.eventId)
|
||||
}
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
class Holder : BaseHolder(STUB_ID) {
|
||||
val avatarImageView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||
|
|
|
@ -50,6 +50,8 @@ abstract class PollItem : AbsMessageItem<PollItem.Holder>() {
|
|||
@EpoxyAttribute
|
||||
lateinit var optionViewStates: List<PollOptionViewState>
|
||||
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
val relatedEventId = eventId ?: return
|
||||
|
|
|
@ -22,7 +22,7 @@ import im.vector.app.R
|
|||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class RedactedMessageItem : AbsMessageItem<RedactedMessageItem.Holder>() {
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
override fun shouldShowReactionAtBottom() = false
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem<StatusTileTimelineIte
|
|||
@EpoxyAttribute
|
||||
lateinit var attributes: Attributes
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun bind(holder: Holder) {
|
||||
|
|
|
@ -51,7 +51,7 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
|
|||
@EpoxyAttribute
|
||||
var callback: TimelineEventController.Callback? = null
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun bind(holder: Holder) {
|
||||
|
|
|
@ -41,7 +41,7 @@ abstract class WidgetTileTimelineItem : AbsBaseMessageItem<WidgetTileTimelineIte
|
|||
@EpoxyAttribute
|
||||
lateinit var attributes: Attributes
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
override fun getViewStubId() = STUB_ID
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun bind(holder: Holder) {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.style
|
||||
|
||||
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners
|
||||
import com.google.android.material.shape.CornerFamily
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
|
||||
fun TimelineMessageLayout.Bubble.CornersRadius.granularRoundedCorners(): GranularRoundedCorners {
|
||||
return GranularRoundedCorners(topStartRadius, topEndRadius, bottomEndRadius, bottomStartRadius)
|
||||
}
|
||||
|
||||
fun TimelineMessageLayout.Bubble.CornersRadius.shapeAppearanceModel(): ShapeAppearanceModel {
|
||||
return ShapeAppearanceModel().toBuilder()
|
||||
.setTopRightCorner(topEndRadius.cornerFamily(), topEndRadius)
|
||||
.setBottomRightCorner(bottomEndRadius.cornerFamily(), bottomEndRadius)
|
||||
.setTopLeftCorner(topStartRadius.cornerFamily(), topStartRadius)
|
||||
.setBottomLeftCorner(bottomStartRadius.cornerFamily(), bottomStartRadius)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun Float.cornerFamily(): Int {
|
||||
return if (this == 0F) CornerFamily.CUT else CornerFamily.ROUNDED
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.style
|
||||
|
||||
enum class TimelineLayoutSettings {
|
||||
MODERN,
|
||||
BUBBLE
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.style
|
||||
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineLayoutSettingsProvider @Inject constructor(private val vectorPreferences: VectorPreferences) {
|
||||
|
||||
fun getLayoutSettings(): TimelineLayoutSettings {
|
||||
return if (vectorPreferences.useMessageBubblesLayout()) {
|
||||
TimelineLayoutSettings.BUBBLE
|
||||
} else {
|
||||
TimelineLayoutSettings.MODERN
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.style
|
||||
|
||||
import android.os.Parcelable
|
||||
import im.vector.app.R
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
sealed interface TimelineMessageLayout : Parcelable {
|
||||
val layoutRes: Int
|
||||
val showAvatar: Boolean
|
||||
val showDisplayName: Boolean
|
||||
val showTimestamp: Boolean
|
||||
|
||||
@Parcelize
|
||||
data class Default(override val showAvatar: Boolean,
|
||||
override val showDisplayName: Boolean,
|
||||
override val showTimestamp: Boolean,
|
||||
// Keep defaultLayout generated on epoxy items
|
||||
override val layoutRes: Int = 0) : TimelineMessageLayout
|
||||
|
||||
@Parcelize
|
||||
data class Bubble(
|
||||
override val showAvatar: Boolean,
|
||||
override val showDisplayName: Boolean,
|
||||
override val showTimestamp: Boolean = true,
|
||||
val isIncoming: Boolean,
|
||||
val isPseudoBubble: Boolean,
|
||||
val cornersRadius: CornersRadius,
|
||||
val timestampAsOverlay: Boolean,
|
||||
override val layoutRes: Int = if (isIncoming) {
|
||||
R.layout.item_timeline_event_bubble_incoming_base
|
||||
} else {
|
||||
R.layout.item_timeline_event_bubble_outgoing_base
|
||||
}
|
||||
) : TimelineMessageLayout {
|
||||
|
||||
@Parcelize
|
||||
data class CornersRadius(
|
||||
val topStartRadius: Float,
|
||||
val topEndRadius: Float,
|
||||
val bottomStartRadius: Float,
|
||||
val bottomEndRadius: Float
|
||||
) : Parcelable
|
||||
}
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.style
|
||||
|
||||
import android.content.res.Resources
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.localDateTime
|
||||
import im.vector.app.core.resources.LocaleProvider
|
||||
import im.vector.app.core.resources.isRTL
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
import org.matrix.android.sdk.api.session.room.timeline.isEdition
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimelineMessageLayoutFactory @Inject constructor(private val session: Session,
|
||||
private val layoutSettingsProvider: TimelineLayoutSettingsProvider,
|
||||
private val localeProvider: LocaleProvider,
|
||||
private val resources: Resources,
|
||||
private val vectorPreferences: VectorPreferences) {
|
||||
|
||||
companion object {
|
||||
// Can be rendered in bubbles, other types will fallback to default
|
||||
private val EVENT_TYPES_WITH_BUBBLE_LAYOUT = setOf(
|
||||
EventType.MESSAGE,
|
||||
EventType.POLL_START,
|
||||
EventType.ENCRYPTED,
|
||||
EventType.STICKER
|
||||
)
|
||||
|
||||
// Can't be rendered in bubbles, so get back to default layout
|
||||
private val MSG_TYPES_WITHOUT_BUBBLE_LAYOUT = setOf(
|
||||
MessageType.MSGTYPE_VERIFICATION_REQUEST
|
||||
)
|
||||
|
||||
// Use the bubble layout but without borders
|
||||
private val MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT = setOf(
|
||||
MessageType.MSGTYPE_IMAGE,
|
||||
MessageType.MSGTYPE_VIDEO,
|
||||
MessageType.MSGTYPE_STICKER_LOCAL,
|
||||
MessageType.MSGTYPE_EMOTE
|
||||
)
|
||||
private val MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY = setOf(
|
||||
MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_VIDEO
|
||||
)
|
||||
}
|
||||
|
||||
private val cornerRadius: Float by lazy {
|
||||
resources.getDimensionPixelSize(R.dimen.chat_bubble_corner_radius).toFloat()
|
||||
}
|
||||
|
||||
private val isRTL: Boolean by lazy {
|
||||
localeProvider.isRTL()
|
||||
}
|
||||
|
||||
fun create(params: TimelineItemFactoryParams): TimelineMessageLayout {
|
||||
val event = params.event
|
||||
val nextDisplayableEvent = params.nextDisplayableEvent
|
||||
val prevDisplayableEvent = params.prevDisplayableEvent
|
||||
val isSentByMe = event.root.senderId == session.myUserId
|
||||
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextDisplayableEvent?.root?.localDateTime()
|
||||
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
|
||||
|
||||
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
|
||||
?: false
|
||||
|
||||
val showInformation = addDaySeparator ||
|
||||
event.senderInfo.avatarUrl != nextDisplayableEvent?.senderInfo?.avatarUrl ||
|
||||
event.senderInfo.disambiguatedDisplayName != nextDisplayableEvent?.senderInfo?.disambiguatedDisplayName ||
|
||||
nextDisplayableEvent.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER, EventType.ENCRYPTED) ||
|
||||
isNextMessageReceivedMoreThanOneHourAgo ||
|
||||
isTileTypeMessage(nextDisplayableEvent) ||
|
||||
nextDisplayableEvent.isEdition()
|
||||
|
||||
val messageLayout = when (layoutSettingsProvider.getLayoutSettings()) {
|
||||
TimelineLayoutSettings.MODERN -> {
|
||||
buildModernLayout(showInformation)
|
||||
}
|
||||
TimelineLayoutSettings.BUBBLE -> {
|
||||
val shouldBuildBubbleLayout = event.shouldBuildBubbleLayout()
|
||||
if (shouldBuildBubbleLayout) {
|
||||
val isFirstFromThisSender = nextDisplayableEvent == null || !nextDisplayableEvent.shouldBuildBubbleLayout() ||
|
||||
nextDisplayableEvent.root.senderId != event.root.senderId || addDaySeparator
|
||||
|
||||
val isLastFromThisSender = prevDisplayableEvent == null || !prevDisplayableEvent.shouldBuildBubbleLayout() ||
|
||||
prevDisplayableEvent.root.senderId != event.root.senderId ||
|
||||
prevDisplayableEvent.root.localDateTime().toLocalDate() != date.toLocalDate()
|
||||
|
||||
val cornersRadius = buildCornersRadius(
|
||||
isIncoming = !isSentByMe,
|
||||
isFirstFromThisSender = isFirstFromThisSender,
|
||||
isLastFromThisSender = isLastFromThisSender
|
||||
)
|
||||
|
||||
val messageContent = event.getLastMessageContent()
|
||||
TimelineMessageLayout.Bubble(
|
||||
showAvatar = showInformation && !isSentByMe,
|
||||
showDisplayName = showInformation && !isSentByMe,
|
||||
isIncoming = !isSentByMe,
|
||||
cornersRadius = cornersRadius,
|
||||
isPseudoBubble = messageContent.isPseudoBubble(),
|
||||
timestampAsOverlay = messageContent.timestampAsOverlay()
|
||||
)
|
||||
} else {
|
||||
buildModernLayout(showInformation)
|
||||
}
|
||||
}
|
||||
}
|
||||
return messageLayout
|
||||
}
|
||||
|
||||
private fun MessageContent?.isPseudoBubble(): Boolean {
|
||||
if (this == null) return false
|
||||
if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline()
|
||||
return this.msgType in MSG_TYPES_WITH_PSEUDO_BUBBLE_LAYOUT
|
||||
}
|
||||
|
||||
private fun MessageContent?.timestampAsOverlay(): Boolean {
|
||||
if (this == null) return false
|
||||
if (msgType == MessageType.MSGTYPE_LOCATION) return vectorPreferences.labsRenderLocationsInTimeline()
|
||||
return this.msgType in MSG_TYPES_WITH_TIMESTAMP_AS_OVERLAY
|
||||
}
|
||||
|
||||
private fun TimelineEvent.shouldBuildBubbleLayout(): Boolean {
|
||||
val type = root.getClearType()
|
||||
if (type in EVENT_TYPES_WITH_BUBBLE_LAYOUT) {
|
||||
val messageContent = getLastMessageContent()
|
||||
return messageContent?.msgType !in MSG_TYPES_WITHOUT_BUBBLE_LAYOUT
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun buildModernLayout(showInformation: Boolean): TimelineMessageLayout.Default {
|
||||
return TimelineMessageLayout.Default(
|
||||
showAvatar = showInformation,
|
||||
showDisplayName = showInformation,
|
||||
showTimestamp = showInformation || vectorPreferences.alwaysShowTimeStamps()
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildCornersRadius(isIncoming: Boolean,
|
||||
isFirstFromThisSender: Boolean,
|
||||
isLastFromThisSender: Boolean): TimelineMessageLayout.Bubble.CornersRadius {
|
||||
return if ((isIncoming && !isRTL) || (!isIncoming && isRTL)) {
|
||||
TimelineMessageLayout.Bubble.CornersRadius(
|
||||
topStartRadius = if (isFirstFromThisSender) cornerRadius else 0f,
|
||||
topEndRadius = cornerRadius,
|
||||
bottomStartRadius = if (isLastFromThisSender) cornerRadius else 0f,
|
||||
bottomEndRadius = cornerRadius
|
||||
)
|
||||
} else {
|
||||
TimelineMessageLayout.Bubble.CornersRadius(
|
||||
topStartRadius = cornerRadius,
|
||||
topEndRadius = if (isFirstFromThisSender) cornerRadius else 0f,
|
||||
bottomStartRadius = cornerRadius,
|
||||
bottomEndRadius = if (isLastFromThisSender) cornerRadius else 0f
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tiles type message never show the sender information (like verification request), so we should repeat it for next message
|
||||
* even if same sender
|
||||
*/
|
||||
private fun isTileTypeMessage(event: TimelineEvent?): Boolean {
|
||||
return when (event?.root?.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL -> true
|
||||
EventType.MESSAGE -> {
|
||||
event.getLastMessageContent() is MessageVerificationRequestContent
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,17 +17,21 @@
|
|||
package im.vector.app.features.home.room.detail.timeline.url
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.setTextOrHide
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.databinding.ViewUrlPreviewBinding
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.view.TimelineMessageLayoutRenderer
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.media.PreviewUrlData
|
||||
|
||||
/**
|
||||
|
@ -37,7 +41,7 @@ class PreviewUrlView @JvmOverloads constructor(
|
|||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : MaterialCardView(context, attrs, defStyleAttr), View.OnClickListener {
|
||||
) : MaterialCardView(context, attrs, defStyleAttr), View.OnClickListener, TimelineMessageLayoutRenderer {
|
||||
|
||||
private lateinit var views: ViewUrlPreviewBinding
|
||||
|
||||
|
@ -47,7 +51,6 @@ class PreviewUrlView @JvmOverloads constructor(
|
|||
setupView()
|
||||
radius = resources.getDimensionPixelSize(R.dimen.preview_url_view_corner_radius).toFloat()
|
||||
cardElevation = 0f
|
||||
setCardBackgroundColor(ThemeUtils.getColor(context, R.attr.vctr_system))
|
||||
}
|
||||
|
||||
private var state: PreviewUrlUiState = PreviewUrlUiState.Unknown
|
||||
|
@ -76,6 +79,22 @@ class PreviewUrlView @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun renderMessageLayout(messageLayout: TimelineMessageLayout) {
|
||||
when (messageLayout) {
|
||||
is TimelineMessageLayout.Default -> {
|
||||
val backgroundColor = ThemeUtils.getColor(context, R.attr.vctr_system)
|
||||
setCardBackgroundColor(backgroundColor)
|
||||
val guidelineBegin = DimensionConverter(resources).dpToPx(8)
|
||||
views.urlPreviewStartGuideline.setGuidelineBegin(guidelineBegin)
|
||||
}
|
||||
is TimelineMessageLayout.Bubble -> {
|
||||
setCardBackgroundColor(Color.TRANSPARENT)
|
||||
rippleColor = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
views.urlPreviewStartGuideline.setGuidelineBegin(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onClick(v: View?) {
|
||||
when (val finalState = state) {
|
||||
is PreviewUrlUiState.Data -> delegate?.onPreviewUrlClicked(finalState.url)
|
||||
|
@ -127,7 +146,7 @@ class PreviewUrlView @JvmOverloads constructor(
|
|||
isVisible = true
|
||||
|
||||
views.urlPreviewTitle.setTextOrHide(previewUrlData.title)
|
||||
views.urlPreviewImage.isVisible = previewUrlData.mxcUrl?.let { imageContentRenderer.render(it, views.urlPreviewImage) }.orFalse()
|
||||
views.urlPreviewImage.isVisible = imageContentRenderer.render(previewUrlData, views.urlPreviewImage)
|
||||
views.urlPreviewDescription.setTextOrHide(previewUrlData.description)
|
||||
views.urlPreviewDescription.maxLines = when {
|
||||
previewUrlData.mxcUrl != null -> 2
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright (c) 2021 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.graphics.drawable.RippleDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewOutlineProvider
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.withStyledAttributes
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.LocaleProvider
|
||||
import im.vector.app.core.resources.getLayoutDirectionFromCurrentLocale
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.databinding.ViewMessageBubbleBinding
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
import im.vector.app.features.home.room.detail.timeline.style.shapeAppearanceModel
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import timber.log.Timber
|
||||
|
||||
class MessageBubbleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0) :
|
||||
RelativeLayout(context, attrs, defStyleAttr), TimelineMessageLayoutRenderer {
|
||||
|
||||
private var isIncoming: Boolean = false
|
||||
|
||||
private val horizontalStubPadding = DimensionConverter(resources).dpToPx(12)
|
||||
private val verticalStubPadding = DimensionConverter(resources).dpToPx(4)
|
||||
|
||||
private lateinit var views: ViewMessageBubbleBinding
|
||||
private lateinit var bubbleDrawable: MaterialShapeDrawable
|
||||
private lateinit var rippleMaskDrawable: MaterialShapeDrawable
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.view_message_bubble, this)
|
||||
context.withStyledAttributes(attrs, R.styleable.MessageBubble) {
|
||||
isIncoming = getBoolean(R.styleable.MessageBubble_incoming_style, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFinishInflate() {
|
||||
super.onFinishInflate()
|
||||
views = ViewMessageBubbleBinding.bind(this)
|
||||
val currentLayoutDirection = LocaleProvider(resources).getLayoutDirectionFromCurrentLocale()
|
||||
val layoutDirectionToSet = if (isIncoming) {
|
||||
currentLayoutDirection
|
||||
} else {
|
||||
if (currentLayoutDirection == View.LAYOUT_DIRECTION_LTR) {
|
||||
View.LAYOUT_DIRECTION_RTL
|
||||
} else {
|
||||
View.LAYOUT_DIRECTION_LTR
|
||||
}
|
||||
}
|
||||
views.informationBottom.layoutDirection = layoutDirectionToSet
|
||||
views.messageThreadSummaryContainer.layoutDirection = layoutDirectionToSet
|
||||
views.bubbleWrapper.layoutDirection = layoutDirectionToSet
|
||||
views.bubbleView.layoutDirection = currentLayoutDirection
|
||||
|
||||
bubbleDrawable = MaterialShapeDrawable()
|
||||
rippleMaskDrawable = MaterialShapeDrawable()
|
||||
DrawableCompat.setTint(rippleMaskDrawable, Color.WHITE)
|
||||
views.bubbleView.apply {
|
||||
outlineProvider = ViewOutlineProvider.BACKGROUND
|
||||
clipToOutline = true
|
||||
background = RippleDrawable(
|
||||
ContextCompat.getColorStateList(context, R.color.mtrl_btn_ripple_color) ?: ColorStateList.valueOf(Color.TRANSPARENT),
|
||||
bubbleDrawable,
|
||||
rippleMaskDrawable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun renderMessageLayout(messageLayout: TimelineMessageLayout) {
|
||||
if (messageLayout !is TimelineMessageLayout.Bubble) {
|
||||
Timber.v("Can't render messageLayout $messageLayout")
|
||||
return
|
||||
}
|
||||
updateDrawables(messageLayout)
|
||||
ConstraintSet().apply {
|
||||
clone(views.bubbleView)
|
||||
clear(R.id.viewStubContainer, ConstraintSet.END)
|
||||
if (messageLayout.timestampAsOverlay) {
|
||||
val timeColor = ContextCompat.getColor(context, R.color.palette_white)
|
||||
views.messageTimeView.setTextColor(timeColor)
|
||||
connect(R.id.viewStubContainer, ConstraintSet.END, R.id.parent, ConstraintSet.END, 0)
|
||||
} else {
|
||||
val timeColor = ThemeUtils.getColor(context, R.attr.vctr_content_tertiary)
|
||||
views.messageTimeView.setTextColor(timeColor)
|
||||
connect(R.id.viewStubContainer, ConstraintSet.END, R.id.messageTimeView, ConstraintSet.START, 0)
|
||||
}
|
||||
applyTo(views.bubbleView)
|
||||
}
|
||||
if (messageLayout.timestampAsOverlay) {
|
||||
views.messageOverlayView.isVisible = true
|
||||
(views.messageOverlayView.background as? GradientDrawable)?.cornerRadii = messageLayout.cornersRadius.toFloatArray()
|
||||
} else {
|
||||
views.messageOverlayView.isVisible = false
|
||||
}
|
||||
if (messageLayout.isPseudoBubble && messageLayout.timestampAsOverlay) {
|
||||
views.viewStubContainer.root.setPadding(0, 0, 0, 0)
|
||||
} else {
|
||||
views.viewStubContainer.root.setPadding(horizontalStubPadding, verticalStubPadding, horizontalStubPadding, verticalStubPadding)
|
||||
}
|
||||
if (isIncoming) {
|
||||
views.messageEndGuideline.updateLayoutParams<LayoutParams> {
|
||||
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end)
|
||||
}
|
||||
views.messageStartGuideline.updateLayoutParams<LayoutParams> {
|
||||
marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start)
|
||||
}
|
||||
} else {
|
||||
views.messageEndGuideline.updateLayoutParams<LayoutParams> {
|
||||
marginEnd = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_start)
|
||||
}
|
||||
views.messageStartGuideline.updateLayoutParams<LayoutParams> {
|
||||
marginStart = resources.getDimensionPixelSize(R.dimen.chat_bubble_margin_end)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun TimelineMessageLayout.Bubble.CornersRadius.toFloatArray(): FloatArray {
|
||||
return floatArrayOf(topStartRadius, topStartRadius, topEndRadius, topEndRadius, bottomEndRadius, bottomEndRadius, bottomStartRadius, bottomStartRadius)
|
||||
}
|
||||
|
||||
private fun updateDrawables(messageLayout: TimelineMessageLayout.Bubble) {
|
||||
val shapeAppearanceModel = messageLayout.cornersRadius.shapeAppearanceModel()
|
||||
bubbleDrawable.apply {
|
||||
this.shapeAppearanceModel = shapeAppearanceModel
|
||||
this.fillColor = if (messageLayout.isPseudoBubble) {
|
||||
ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
} else {
|
||||
val backgroundColorAttr = if (isIncoming) R.attr.vctr_message_bubble_inbound else R.attr.vctr_message_bubble_outbound
|
||||
val backgroundColor = ThemeUtils.getColor(context, backgroundColorAttr)
|
||||
ColorStateList.valueOf(backgroundColor)
|
||||
}
|
||||
}
|
||||
rippleMaskDrawable.shapeAppearanceModel = shapeAppearanceModel
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.view
|
||||
|
||||
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
|
||||
|
||||
interface TimelineMessageLayoutRenderer {
|
||||
fun renderMessageLayout(messageLayout: TimelineMessageLayout)
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.html
|
||||
|
||||
import org.commonmark.node.AbstractVisitor
|
||||
import org.commonmark.node.Code
|
||||
import org.commonmark.node.FencedCodeBlock
|
||||
import org.commonmark.node.IndentedCodeBlock
|
||||
|
||||
/**
|
||||
* This class is in charge of visiting nodes and tells if we have some code nodes (inline or block).
|
||||
*/
|
||||
class CodeVisitor : AbstractVisitor() {
|
||||
|
||||
var codeKind: Kind = Kind.NONE
|
||||
private set
|
||||
|
||||
override fun visit(fencedCodeBlock: FencedCodeBlock?) {
|
||||
if (codeKind == Kind.NONE) {
|
||||
codeKind = Kind.BLOCK
|
||||
}
|
||||
}
|
||||
|
||||
override fun visit(indentedCodeBlock: IndentedCodeBlock?) {
|
||||
if (codeKind == Kind.NONE) {
|
||||
codeKind = Kind.BLOCK
|
||||
}
|
||||
}
|
||||
|
||||
override fun visit(code: Code?) {
|
||||
if (codeKind == Kind.NONE) {
|
||||
codeKind = Kind.INLINE
|
||||
}
|
||||
}
|
||||
|
||||
enum class Kind {
|
||||
NONE,
|
||||
INLINE,
|
||||
BLOCK
|
||||
}
|
||||
}
|
|
@ -17,9 +17,11 @@
|
|||
package im.vector.app.features.html
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.text.Spannable
|
||||
import androidx.core.text.toSpannable
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import io.noties.markwon.AbstractMarkwonPlugin
|
||||
import io.noties.markwon.Markwon
|
||||
|
@ -53,11 +55,11 @@ class EventHtmlRenderer @Inject constructor(
|
|||
.usePlugin(object : AbstractMarkwonPlugin() { // Markwon expects maths to be in a specific format: https://noties.io/Markwon/docs/v4/ext-latex
|
||||
override fun processMarkdown(markdown: String): String {
|
||||
return markdown
|
||||
.replace(Regex("""<span\s+data-mx-maths="([^"]*)">.*?</span>""")) {
|
||||
matchResult -> "$$" + matchResult.groupValues[1] + "$$"
|
||||
.replace(Regex("""<span\s+data-mx-maths="([^"]*)">.*?</span>""")) { matchResult ->
|
||||
"$$" + matchResult.groupValues[1] + "$$"
|
||||
}
|
||||
.replace(Regex("""<div\s+data-mx-maths="([^"]*)">.*?</div>""")) {
|
||||
matchResult -> "\n$$\n" + matchResult.groupValues[1] + "\n$$\n"
|
||||
.replace(Regex("""<div\s+data-mx-maths="([^"]*)">.*?</div>""")) { matchResult ->
|
||||
"\n$$\n" + matchResult.groupValues[1] + "\n$$\n"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -112,12 +114,15 @@ class EventHtmlRenderer @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider) : HtmlPlugin.HtmlConfigure {
|
||||
class MatrixHtmlPluginConfigure @Inject constructor(private val colorProvider: ColorProvider, private val resources: Resources) : HtmlPlugin.HtmlConfigure {
|
||||
|
||||
override fun configureHtml(plugin: HtmlPlugin) {
|
||||
plugin
|
||||
.addHandler(FontTagHandler())
|
||||
.addHandler(ParagraphHandler(DimensionConverter(resources)))
|
||||
.addHandler(MxReplyTagHandler())
|
||||
.addHandler(CodePreTagHandler())
|
||||
.addHandler(CodeTagHandler())
|
||||
.addHandler(SpanHandler(colorProvider))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.html
|
||||
|
||||
import io.noties.markwon.MarkwonVisitor
|
||||
import io.noties.markwon.SpannableBuilder
|
||||
import io.noties.markwon.html.HtmlTag
|
||||
import io.noties.markwon.html.MarkwonHtmlRenderer
|
||||
import io.noties.markwon.html.TagHandler
|
||||
|
||||
class CodeTagHandler : TagHandler() {
|
||||
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
HtmlCodeSpan(visitor.configuration().theme(), false),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
}
|
||||
|
||||
override fun supportedTags(): List<String> {
|
||||
return listOf("code")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre tag are already handled by HtmlPlugin to keep the formatting.
|
||||
* We are only using it to check for <pre><code>*</code></pre> tags.
|
||||
*/
|
||||
class CodePreTagHandler : TagHandler() {
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
val htmlCodeSpan = visitor.builder()
|
||||
.getSpans(tag.start(), tag.end())
|
||||
.firstOrNull {
|
||||
it.what is HtmlCodeSpan
|
||||
}
|
||||
if (htmlCodeSpan != null) {
|
||||
(htmlCodeSpan.what as HtmlCodeSpan).isBlock = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun supportedTags(): List<String> {
|
||||
return listOf("pre")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.html
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.text.Layout
|
||||
import android.text.TextPaint
|
||||
import android.text.style.LeadingMarginSpan
|
||||
import android.text.style.MetricAffectingSpan
|
||||
import io.noties.markwon.core.MarkwonTheme
|
||||
|
||||
class HtmlCodeSpan(private val theme: MarkwonTheme, var isBlock: Boolean) : MetricAffectingSpan(), LeadingMarginSpan {
|
||||
|
||||
private val rect = Rect()
|
||||
private val paint = Paint()
|
||||
|
||||
override fun updateDrawState(p: TextPaint) {
|
||||
applyTextStyle(p)
|
||||
if (!isBlock) {
|
||||
p.bgColor = theme.getCodeBackgroundColor(p)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateMeasureState(p: TextPaint) {
|
||||
applyTextStyle(p)
|
||||
}
|
||||
|
||||
private fun applyTextStyle(p: TextPaint) {
|
||||
if (isBlock) {
|
||||
theme.applyCodeBlockTextStyle(p)
|
||||
} else {
|
||||
theme.applyCodeTextStyle(p)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLeadingMargin(first: Boolean): Int {
|
||||
return theme.codeBlockMargin
|
||||
}
|
||||
|
||||
override fun drawLeadingMargin(
|
||||
c: Canvas,
|
||||
p: Paint?,
|
||||
x: Int,
|
||||
dir: Int,
|
||||
top: Int,
|
||||
baseline: Int,
|
||||
bottom: Int,
|
||||
text: CharSequence?,
|
||||
start: Int,
|
||||
end: Int,
|
||||
first: Boolean,
|
||||
layout: Layout?
|
||||
) {
|
||||
if (!isBlock) return
|
||||
|
||||
paint.style = Paint.Style.FILL
|
||||
paint.color = theme.getCodeBlockBackgroundColor(p!!)
|
||||
val left: Int
|
||||
val right: Int
|
||||
if (dir > 0) {
|
||||
left = x
|
||||
right = c.width
|
||||
} else {
|
||||
left = x - c.width
|
||||
right = x
|
||||
}
|
||||
rect[left, top, right] = bottom
|
||||
c.drawRect(rect, paint)
|
||||
}
|
||||
}
|
|
@ -17,28 +17,17 @@
|
|||
package im.vector.app.features.html
|
||||
|
||||
import io.noties.markwon.MarkwonVisitor
|
||||
import io.noties.markwon.SpannableBuilder
|
||||
import io.noties.markwon.html.HtmlTag
|
||||
import io.noties.markwon.html.MarkwonHtmlRenderer
|
||||
import io.noties.markwon.html.TagHandler
|
||||
import org.commonmark.node.BlockQuote
|
||||
|
||||
class MxReplyTagHandler : TagHandler() {
|
||||
|
||||
override fun supportedTags() = listOf("mx-reply")
|
||||
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
val configuration = visitor.configuration()
|
||||
val factory = configuration.spansFactory().get(BlockQuote::class.java)
|
||||
if (factory != null) {
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
factory.getSpans(configuration, visitor.renderProps()),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
val replyText = visitor.builder().removeFromEnd(tag.end())
|
||||
visitor.builder().append("\n\n").append(replyText)
|
||||
}
|
||||
visitChildren(visitor, renderer, tag.asBlock)
|
||||
val replyText = visitor.builder().removeFromEnd(tag.end())
|
||||
visitor.builder().append("\n\n").append(replyText)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.html
|
||||
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import io.noties.markwon.MarkwonVisitor
|
||||
import io.noties.markwon.SpannableBuilder
|
||||
import io.noties.markwon.html.HtmlTag
|
||||
import io.noties.markwon.html.MarkwonHtmlRenderer
|
||||
import io.noties.markwon.html.TagHandler
|
||||
import me.gujun.android.span.style.VerticalPaddingSpan
|
||||
|
||||
class ParagraphHandler(private val dimensionConverter: DimensionConverter) : TagHandler() {
|
||||
|
||||
override fun supportedTags() = listOf("p")
|
||||
|
||||
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
|
||||
if (tag.isBlock) {
|
||||
visitChildren(visitor, renderer, tag.asBlock)
|
||||
}
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
VerticalPaddingSpan(dimensionConverter.dpToPx(4), dimensionConverter.dpToPx(4)),
|
||||
tag.start(),
|
||||
tag.end()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -65,10 +65,15 @@ class PillImageSpan(private val glideRequests: GlideRequests,
|
|||
fm: Paint.FontMetricsInt?): Int {
|
||||
val rect = pillDrawable.bounds
|
||||
if (fm != null) {
|
||||
fm.ascent = -rect.bottom
|
||||
fm.descent = 0
|
||||
fm.top = fm.ascent
|
||||
fm.bottom = 0
|
||||
val fmPaint = paint.fontMetricsInt
|
||||
val fontHeight = fmPaint.bottom - fmPaint.top
|
||||
val drHeight = rect.bottom - rect.top
|
||||
val top = drHeight / 2 - fontHeight / 4
|
||||
val bottom = drHeight / 2 + fontHeight / 4
|
||||
fm.ascent = -bottom
|
||||
fm.top = -bottom
|
||||
fm.bottom = top
|
||||
fm.descent = top
|
||||
}
|
||||
return rect.right
|
||||
}
|
||||
|
@ -82,7 +87,9 @@ class PillImageSpan(private val glideRequests: GlideRequests,
|
|||
bottom: Int,
|
||||
paint: Paint) {
|
||||
canvas.save()
|
||||
val transY = bottom - pillDrawable.bounds.bottom
|
||||
val fm = paint.fontMetricsInt
|
||||
val transY: Int = y + (fm.descent + fm.ascent - pillDrawable.bounds.bottom) / 2
|
||||
canvas.save()
|
||||
canvas.translate(x, transY.toFloat())
|
||||
pillDrawable.draw(canvas)
|
||||
canvas.restore()
|
||||
|
|
|
@ -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 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_TIMELINE = 17.0
|
||||
|
|
|
@ -121,7 +121,7 @@ class LocationPreviewFragment @Inject constructor(
|
|||
MapState(
|
||||
zoomOnlyOnce = true,
|
||||
pinLocationData = location,
|
||||
pinId = args.locationOwnerId,
|
||||
pinId = args.locationOwnerId ?: DEFAULT_PIN_ID,
|
||||
pinDrawable = pinDrawable
|
||||
)
|
||||
)
|
||||
|
|
|
@ -30,7 +30,7 @@ data class LocationSharingArgs(
|
|||
val roomId: String,
|
||||
val mode: LocationSharingMode,
|
||||
val initialLocationData: LocationData?,
|
||||
val locationOwnerId: String
|
||||
val locationOwnerId: String?
|
||||
) : Parcelable
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
|
|
@ -118,8 +118,4 @@ class LocationSharingFragment @Inject constructor(
|
|||
views.mapView.render(state.toMapState())
|
||||
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(
|
||||
zoomOnlyOnce = true,
|
||||
pinLocationData = lastKnownLocation,
|
||||
pinId = LocationSharingFragment.USER_PIN_NAME,
|
||||
pinId = DEFAULT_PIN_ID,
|
||||
pinDrawable = pinDrawable
|
||||
)
|
||||
|
|
|
@ -16,13 +16,13 @@
|
|||
|
||||
package im.vector.app.features.location
|
||||
|
||||
import android.content.res.Resources
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.LocaleProvider
|
||||
import im.vector.app.core.resources.isRTL
|
||||
import javax.inject.Inject
|
||||
|
||||
class UrlMapProvider @Inject constructor(
|
||||
private val resources: Resources
|
||||
private val localeProvider: LocaleProvider
|
||||
) {
|
||||
private val keyParam = "?key=${BuildConfig.mapTilerKey}"
|
||||
|
||||
|
@ -49,7 +49,7 @@ class UrlMapProvider @Inject constructor(
|
|||
append(height)
|
||||
append(".png")
|
||||
append(keyParam)
|
||||
if (!resources.getBoolean(R.bool.is_rtl)) {
|
||||
if (!localeProvider.isRTL()) {
|
||||
// On LTR languages we want the legal mentions to be displayed on the bottom left of the image
|
||||
append("&attribution=bottomleft")
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.features.media
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
|
@ -23,6 +24,7 @@ import android.view.View
|
|||
import android.widget.ImageView
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.Transformation
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
|
@ -42,6 +44,7 @@ import im.vector.app.core.utils.DimensionConverter
|
|||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
import org.matrix.android.sdk.api.session.media.PreviewUrlData
|
||||
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
@ -59,6 +62,9 @@ interface AttachmentData : Parcelable {
|
|||
val allowNonMxcUrls: Boolean
|
||||
}
|
||||
|
||||
private const val URL_PREVIEW_IMAGE_MIN_FULL_WIDTH_PX = 600
|
||||
private const val URL_PREVIEW_IMAGE_MIN_FULL_HEIGHT_PX = 315
|
||||
|
||||
class ImageContentRenderer @Inject constructor(private val localFilesHelper: LocalFilesHelper,
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val dimensionConverter: DimensionConverter) {
|
||||
|
@ -87,12 +93,20 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
|
|||
/**
|
||||
* For url preview
|
||||
*/
|
||||
fun render(mxcUrl: String, imageView: ImageView): Boolean {
|
||||
fun render(previewUrlData: PreviewUrlData, imageView: ImageView): Boolean {
|
||||
val contentUrlResolver = activeSessionHolder.getActiveSession().contentUrlResolver()
|
||||
val imageUrl = contentUrlResolver.resolveFullSize(mxcUrl) ?: return false
|
||||
|
||||
val imageUrl = contentUrlResolver.resolveFullSize(previewUrlData.mxcUrl) ?: return false
|
||||
val maxHeight = dimensionConverter.resources.getDimensionPixelSize(R.dimen.preview_url_view_image_max_height)
|
||||
val height = previewUrlData.imageHeight ?: URL_PREVIEW_IMAGE_MIN_FULL_HEIGHT_PX
|
||||
val width = previewUrlData.imageWidth ?: URL_PREVIEW_IMAGE_MIN_FULL_WIDTH_PX
|
||||
if (height < URL_PREVIEW_IMAGE_MIN_FULL_HEIGHT_PX || width < URL_PREVIEW_IMAGE_MIN_FULL_WIDTH_PX) {
|
||||
imageView.scaleType = ImageView.ScaleType.CENTER_INSIDE
|
||||
} else {
|
||||
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
}
|
||||
GlideApp.with(imageView)
|
||||
.load(imageUrl)
|
||||
.override(width, height.coerceAtMost(maxHeight))
|
||||
.into(imageView)
|
||||
return true
|
||||
}
|
||||
|
@ -109,7 +123,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
|
|||
.into(imageView)
|
||||
}
|
||||
|
||||
fun render(data: Data, mode: Mode, imageView: ImageView) {
|
||||
fun render(data: Data, mode: Mode, imageView: ImageView, cornerTransformation: Transformation<Bitmap> = RoundedCorners(dimensionConverter.dpToPx(8))) {
|
||||
val size = processSize(data, mode)
|
||||
imageView.updateLayoutParams {
|
||||
width = size.width
|
||||
|
@ -120,7 +134,7 @@ class ImageContentRenderer @Inject constructor(private val localFilesHelper: Loc
|
|||
|
||||
createGlideRequest(data, mode, imageView, size)
|
||||
.dontAnimate()
|
||||
.transform(RoundedCorners(dimensionConverter.dpToPx(8)))
|
||||
.transform(cornerTransformation)
|
||||
// .thumbnail(0.3f)
|
||||
.into(imageView)
|
||||
}
|
||||
|
|
|
@ -556,7 +556,7 @@ class DefaultNavigator @Inject constructor(
|
|||
roomId: String,
|
||||
mode: LocationSharingMode,
|
||||
initialLocationData: LocationData?,
|
||||
locationOwnerId: String) {
|
||||
locationOwnerId: String?) {
|
||||
val intent = LocationSharingActivity.getIntent(
|
||||
context,
|
||||
LocationSharingArgs(roomId = roomId, mode = mode, initialLocationData = initialLocationData, locationOwnerId = locationOwnerId)
|
||||
|
|
|
@ -162,7 +162,8 @@ interface Navigator {
|
|||
roomId: String,
|
||||
mode: LocationSharingMode,
|
||||
initialLocationData: LocationData?,
|
||||
locationOwnerId: String)
|
||||
locationOwnerId: String?)
|
||||
|
||||
fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null)
|
||||
|
||||
fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs)
|
||||
|
|
|
@ -70,6 +70,7 @@ class ReactionButton @JvmOverloads constructor(context: Context,
|
|||
orientation = HORIZONTAL
|
||||
minimumHeight = DimensionConverter(context.resources).dpToPx(30)
|
||||
gravity = Gravity.CENTER
|
||||
layoutDirection = View.LAYOUT_DIRECTION_LOCALE
|
||||
views = ReactionButtonBinding.bind(this)
|
||||
views.reactionCount.text = TextUtils.formatCountToShortDecimal(reactionCount)
|
||||
context.withStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr) {
|
||||
|
|
|
@ -83,6 +83,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
|||
// interface
|
||||
const val SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY = "SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY"
|
||||
const val SETTINGS_INTERFACE_TEXT_SIZE_KEY = "SETTINGS_INTERFACE_TEXT_SIZE_KEY"
|
||||
const val SETTINGS_INTERFACE_BUBBLE_KEY = "SETTINGS_INTERFACE_BUBBLE_KEY"
|
||||
const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY"
|
||||
private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY"
|
||||
private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY"
|
||||
|
@ -849,6 +850,15 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
|||
return defaultPrefs.getBoolean(SETTINGS_SHOW_EMOJI_KEYBOARD, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the timeline messages should be shown in a bubble or not.
|
||||
*
|
||||
* @return true to show timeline message in bubble.
|
||||
*/
|
||||
fun useMessageBubblesLayout(): Boolean {
|
||||
return defaultPrefs.getBoolean(SETTINGS_INTERFACE_BUBBLE_KEY, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the rage shake is used.
|
||||
*
|
||||
|
|
|
@ -53,7 +53,6 @@ class RoomWidgetPermissionBottomSheet :
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
setupViews()
|
||||
}
|
||||
|
||||
|
|
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
|
||||
android:id="@+id/useCaseOptionOne"
|
||||
style="@style/Widget.Vector.TextView.Subtitle"
|
||||
style="@style/Widget.Vector.TextView.Subtitle.Medium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
|
@ -97,7 +97,7 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/useCaseOptionTwo"
|
||||
style="@style/Widget.Vector.TextView.Subtitle"
|
||||
style="@style/Widget.Vector.TextView.Subtitle.Medium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
|
@ -115,7 +115,7 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/useCaseOptionThree"
|
||||
style="@style/Widget.Vector.TextView.Subtitle"
|
||||
style="@style/Widget.Vector.TextView.Subtitle.Medium"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
|
@ -133,10 +133,12 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/useCaseSkip"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:gravity="center"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
app:layout_constraintBottom_toTopOf="@id/contentFooterSpacing"
|
||||
app:layout_constraintEnd_toEndOf="@id/useCaseGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/useCaseGutterStart"
|
||||
|
@ -153,10 +155,12 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/useCaseFooter"
|
||||
style="@style/Widget.Vector.TextView.Subtitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/ftue_auth_use_case_join_existing_server"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
app:layout_constraintBottom_toTopOf="@id/useCaseConnectToServer"
|
||||
app:layout_constraintEnd_toEndOf="@id/useCaseGutterEnd"
|
||||
app:layout_constraintStart_toStartOf="@id/useCaseGutterStart"
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
android:singleLine="true"
|
||||
android:textColor="?vctr_content_primary"
|
||||
android:textStyle="bold"
|
||||
android:textAlignment="viewStart"
|
||||
app:layout_constraintEnd_toStartOf="@id/bottom_sheet_message_preview_timestamp"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||
app:layout_constraintTop_toTopOf="@id/bottom_sheet_message_preview_avatar"
|
||||
|
@ -78,6 +79,7 @@
|
|||
android:maxLines="3"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
android:textIsSelectable="false"
|
||||
android:textAlignment="viewStart"
|
||||
app:layout_constraintBottom_toTopOf="@id/bottom_sheet_message_preview_body_details"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar"
|
||||
|
@ -96,6 +98,7 @@
|
|||
android:textColor="?vctr_content_tertiary"
|
||||
android:textIsSelectable="false"
|
||||
android:visibility="gone"
|
||||
android:textAlignment="viewStart"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="@id/bottom_sheet_message_preview_body"
|
||||
app:layout_constraintStart_toStartOf="@id/bottom_sheet_message_preview_body"
|
||||
|
|
|
@ -190,6 +190,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:textAlignment="viewStart"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textColor="?vctr_content_secondary"
|
||||
|
@ -202,6 +203,7 @@
|
|||
android:id="@+id/roomTypingView"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="0dp"
|
||||
android:textAlignment="viewStart"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:textAlignment="viewStart"
|
||||
android:layout_toStartOf="@id/messageTimeView"
|
||||
android:layout_toEndOf="@id/messageStartGuideline"
|
||||
android:ellipsize="end"
|
||||
|
@ -76,66 +77,16 @@
|
|||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<FrameLayout
|
||||
<include
|
||||
android:id="@+id/viewStubContainer"
|
||||
android:layout_width="match_parent"
|
||||
layout="@layout/item_timeline_event_view_stubs_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/messageMemberNameView"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_toStartOf="@id/messageSendStateImageView"
|
||||
android:layout_toEndOf="@id/messageStartGuideline"
|
||||
android:addStatesFromChildren="true">
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentTextStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/item_timeline_event_text_message_stub"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentCodeBlockStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/item_timeline_event_code_block_stub"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentMediaStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout_height="wrap_content"
|
||||
android:inflatedId="@+id/messageContentMedia"
|
||||
android:layout="@layout/item_timeline_event_media_message_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentFileStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/item_timeline_event_file_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentRedactedStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/item_timeline_event_redacted_stub" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentVoiceStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout="@layout/item_timeline_event_voice_stub"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentPollStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout="@layout/item_timeline_event_poll" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/messageContentLocationStub"
|
||||
style="@style/TimelineContentStubBaseParams"
|
||||
android:layout="@layout/item_timeline_event_location_stub" />
|
||||
|
||||
</FrameLayout>
|
||||
android:addStatesFromChildren="true" />
|
||||
|
||||
<im.vector.app.core.ui.views.SendStateImageView
|
||||
android:id="@+id/messageSendStateImageView"
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
<FrameLayout
|
||||
android:id="@+id/viewStubContainer"
|
||||
style="@style/TimelineContentStubContainerParams"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
|
||||
<FrameLayout
|
||||
android:id="@+id/viewStubContainer"
|
||||
style="@style/TimelineContentStubContainerParams"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<im.vector.app.features.home.room.detail.timeline.view.MessageBubbleView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:addStatesFromChildren="true"
|
||||
app:incoming_style="true" />
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<im.vector.app.features.home.room.detail.timeline.view.MessageBubbleView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:addStatesFromChildren="true"
|
||||
app:incoming_style="false" />
|
|
@ -19,9 +19,8 @@
|
|||
<TextView
|
||||
android:id="@+id/itemDefaultTextView"
|
||||
style="@style/Widget.Vector.TextView.Body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue