Message bubble footer area
For use with - bottom timestamps in message bubbles - unpersonal read receipts: maybe in the future, as we can not really update all relevant messages with the current db update mechanism. But we can use it for send-status for now. Change-Id: I2de909362394e336f9aaba9f0d157e7c6fe8f9b1
This commit is contained in:
parent
5853709c97
commit
8602f1345b
@ -0,0 +1,68 @@
|
||||
package im.vector.app.core.ui.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* TextView that reserves space at the bottom for overlaying it with a footer, e.g. in a FrameLayout or RelativeLayout
|
||||
*/
|
||||
class FooteredTextView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
): AppCompatTextView(context, attrs, defStyleAttr) {
|
||||
|
||||
var footerHeight: Int = 0
|
||||
var footerWidth: Int = 0
|
||||
//var widthLimit: Float = 0f
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
// First, let super measure the content for our normal TextView use
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
// Get max available width
|
||||
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
||||
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
||||
val widthLimit = if (widthMode == MeasureSpec.AT_MOST) { widthSize.toFloat() } else { Float.MAX_VALUE }
|
||||
/*
|
||||
// Sometimes, widthLimit is not the actual limit, so remember it... ?
|
||||
if (this.widthLimit > widthLimit) {
|
||||
widthLimit = this.widthLimit
|
||||
} else {
|
||||
this.widthLimit = widthLimit
|
||||
}
|
||||
*/
|
||||
|
||||
// Get required width for all lines
|
||||
var maxLineWidth = 0f
|
||||
for (i in 0 until layout.lineCount) {
|
||||
maxLineWidth = max(layout.getLineWidth(i), maxLineWidth)
|
||||
}
|
||||
|
||||
// Fix wrap_content in multi-line texts by using maxLineWidth instead of measuredWidth here
|
||||
// (compare WrapWidthTextView.kt)
|
||||
var newWidth = ceil(maxLineWidth).toInt()
|
||||
var newHeight = measuredHeight
|
||||
|
||||
val widthLastLine = layout.getLineWidth(layout.lineCount-1)
|
||||
|
||||
// Required width if putting footer in the same line as the last line
|
||||
val widthWithHorizontalFooter = widthLastLine + footerWidth
|
||||
|
||||
// Is there space for a horizontal footer?
|
||||
if (widthWithHorizontalFooter <= widthLimit) {
|
||||
// Reserve extra horizontal footer space if necessary
|
||||
if (widthWithHorizontalFooter > newWidth) {
|
||||
newWidth = ceil(widthWithHorizontalFooter).toInt()
|
||||
}
|
||||
} else {
|
||||
// Reserve vertical footer space
|
||||
newHeight += footerHeight
|
||||
}
|
||||
|
||||
setMeasuredDimension(newWidth, newHeight)
|
||||
}
|
||||
}
|
@ -18,9 +18,11 @@
|
||||
|
||||
package im.vector.app.features.home.room.detail.timeline.helper
|
||||
|
||||
import android.content.Context;
|
||||
import im.vector.app.core.date.DateFormatKind
|
||||
import im.vector.app.core.date.VectorDateFormatter
|
||||
import im.vector.app.core.extensions.localDateTime
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AnonymousReadReceipt
|
||||
import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
|
||||
@ -28,6 +30,7 @@ import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.themes.BubbleThemeUtils
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
@ -49,7 +52,8 @@ import javax.inject.Inject
|
||||
class MessageInformationDataFactory @Inject constructor(private val session: Session,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
private val vectorPreferences: VectorPreferences) {
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val context: Context) {
|
||||
|
||||
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
|
||||
// Non nullability has been tested before
|
||||
@ -81,7 +85,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
avatarUrl = event.senderInfo.avatarUrl,
|
||||
memberName = event.senderInfo.disambiguatedDisplayName,
|
||||
showInformation = showInformation,
|
||||
forceShowTimestamp = vectorPreferences.alwaysShowTimeStamps(),
|
||||
forceShowTimestamp = vectorPreferences.alwaysShowTimeStamps() || BubbleThemeUtils.forceAlwaysShowTimestamps(context),
|
||||
orderedReactionList = event.annotations?.reactionsSummary
|
||||
// ?.filter { isSingleEmoji(it.key) }
|
||||
?.map {
|
||||
@ -113,6 +117,16 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
||||
ReferencesInfoData(verificationState)
|
||||
},
|
||||
sentByMe = event.root.senderId == session.myUserId,
|
||||
readReceiptAnonymous = if (event.root.sendState == SendState.SYNCED) {
|
||||
/*if (event.readByOther) {
|
||||
AnonymousReadReceipt.READ
|
||||
} else {
|
||||
AnonymousReadReceipt.SENT
|
||||
}*/
|
||||
AnonymousReadReceipt.NONE
|
||||
} else {
|
||||
AnonymousReadReceipt.PROCESSING
|
||||
},
|
||||
isDirect = roomSummaryHolder.roomSummary?.isDirect ?: false,
|
||||
e2eDecoration = e2eDecoration
|
||||
)
|
||||
|
@ -18,18 +18,16 @@ package im.vector.app.features.home.room.detail.timeline.item
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import kotlin.math.max
|
||||
import kotlin.math.round
|
||||
@ -40,6 +38,7 @@ 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.themes.BubbleThemeUtils
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
* Base timeline item that adds an optional information bar with the sender avatar, name and time
|
||||
@ -66,7 +65,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
|
||||
val avatarImageView: ImageView?
|
||||
val memberNameView: TextView?
|
||||
val timeView: TextView?
|
||||
var timeView: TextView?
|
||||
val hiddenViews = ArrayList<View>()
|
||||
val invisibleViews = ArrayList<View>()
|
||||
|
||||
@ -108,6 +107,24 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
hiddenViews.add(holder.timeView)
|
||||
hiddenViews.add(holder.bubbleTimeView)
|
||||
}
|
||||
|
||||
if (timeView === holder.bubbleTimeView) {
|
||||
// We have two possible bubble time view locations
|
||||
// For code readability, we don't inline this setting in the above cases
|
||||
if (BubbleThemeUtils.getBubbleTimeLocation(holder.bubbleTimeView.context) == BubbleThemeUtils.BUBBLE_TIME_BOTTOM) {
|
||||
timeView = holder.bubbleFooterTimeView
|
||||
if (attributes.informationData.showInformation) {
|
||||
// Don't hide, so our relative layout rules still work
|
||||
invisibleViews.add(holder.bubbleTimeView)
|
||||
} else {
|
||||
// Do hide, or we accidentally reserve space
|
||||
hiddenViews.add(holder.bubbleTimeView)
|
||||
}
|
||||
} else {
|
||||
hiddenViews.add(holder.bubbleFooterTimeView)
|
||||
}
|
||||
}
|
||||
|
||||
// Dual-side bubbles: hide own avatar, and all in direct chats
|
||||
if ((!attributes.informationData.showInformation) ||
|
||||
(contentInBubble && (attributes.informationData.sentByMe || attributes.informationData.isDirect))) {
|
||||
@ -117,15 +134,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
avatarImageView = holder.avatarImageView
|
||||
}
|
||||
|
||||
hiddenViews.forEach {
|
||||
// Same as it.isVisible = false
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
invisibleViews.forEach {
|
||||
// Same as it.isInvisible = true
|
||||
it.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
// Views available in upstream Element
|
||||
avatarImageView?.layoutParams = avatarImageView?.layoutParams?.apply {
|
||||
height = attributes.avatarSize
|
||||
@ -143,8 +151,23 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
avatarImageView?.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
memberNameView?.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
|
||||
// Views added by Schildi
|
||||
// More extra views added by Schildi
|
||||
holder.viewStubContainer.minimumWidth = getViewStubMinimumWidth(holder, contentInBubble, attributes.informationData.showInformation)
|
||||
if (contentInBubble) {
|
||||
holder.bubbleFootView.visibility = View.VISIBLE
|
||||
} else {
|
||||
hiddenViews.add(holder.bubbleFootView)
|
||||
}
|
||||
|
||||
// Actually hide all unnecessary views
|
||||
hiddenViews.forEach {
|
||||
// Same as it.isVisible = false
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
invisibleViews.forEach {
|
||||
// Same as it.isInvisible = true
|
||||
it.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbind(holder: H) {
|
||||
@ -166,6 +189,9 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
val bubbleView by bind<View>(R.id.bubbleView)
|
||||
val bubbleMemberNameView by bind<TextView>(R.id.bubbleMessageMemberNameView)
|
||||
val bubbleTimeView by bind<TextView>(R.id.bubbleMessageTimeView)
|
||||
val bubbleFootView by bind<LinearLayout>(R.id.bubbleFootView)
|
||||
val bubbleFooterTimeView by bind<TextView>(R.id.bubbleFooterMessageTimeView)
|
||||
val bubbleFooterReadReceipt by bind<ImageView>(R.id.bubbleFooterReadReceipt)
|
||||
val viewStubContainer by bind<FrameLayout>(R.id.viewStubContainer)
|
||||
}
|
||||
|
||||
@ -211,20 +237,33 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
}
|
||||
|
||||
open fun getViewStubMinimumWidth(holder: H, contentInBubble: Boolean, showInformation: Boolean): Int {
|
||||
return if (contentInBubble && (attributes.informationData.showInformation || attributes.informationData.forceShowTimestamp)) {
|
||||
// Guess text width for name and time
|
||||
val text = if (attributes.informationData.showInformation) {
|
||||
holder.bubbleMemberNameView.text.toString() + " " + holder.bubbleTimeView.text.toString()
|
||||
return if (contentInBubble) {
|
||||
if (BubbleThemeUtils.getBubbleTimeLocation(holder.bubbleTimeView.context) == BubbleThemeUtils.BUBBLE_TIME_BOTTOM) {
|
||||
if (attributes.informationData.showInformation) {
|
||||
// Since timeView automatically gets enough space, either within or outside the viewStub, we just need to ensure the member name view has enough space
|
||||
// Somehow not enough without extra space...
|
||||
ceil(BubbleThemeUtils.guessTextWidth(holder.bubbleMemberNameView, holder.bubbleMemberNameView.text.toString() + " ")).toInt()
|
||||
} else {
|
||||
// wrap_content works!
|
||||
0
|
||||
}
|
||||
} else if (attributes.informationData.showInformation || attributes.informationData.forceShowTimestamp) {
|
||||
// Guess text width for name and time next to each other
|
||||
val text = if (attributes.informationData.showInformation) {
|
||||
holder.bubbleMemberNameView.text.toString() + " " + holder.bubbleTimeView.text.toString()
|
||||
} else {
|
||||
holder.bubbleTimeView.text.toString()
|
||||
}
|
||||
val textSize = if (attributes.informationData.showInformation) {
|
||||
max(holder.bubbleMemberNameView.textSize, holder.bubbleTimeView.textSize)
|
||||
} else {
|
||||
holder.bubbleTimeView.textSize
|
||||
}
|
||||
ceil(BubbleThemeUtils.guessTextWidth(textSize, text)).toInt()
|
||||
} else {
|
||||
holder.bubbleTimeView.text.toString()
|
||||
// Not showing any header, use wrap_content of content only
|
||||
0
|
||||
}
|
||||
val paint = Paint()
|
||||
paint.textSize = if (attributes.informationData.showInformation) {
|
||||
max(holder.bubbleMemberNameView.textSize, holder.bubbleTimeView.textSize)
|
||||
} else {
|
||||
holder.bubbleTimeView.textSize
|
||||
}
|
||||
round(paint.measureText(text)).toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
@ -243,11 +282,23 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
return round(96*density).toInt()
|
||||
}
|
||||
|
||||
open fun allowFooterOverlay(holder: H): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
open fun needsFooterReservation(holder: H): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
open fun reserveFooterSpace(holder: H, width: Int, height: Int) {
|
||||
}
|
||||
|
||||
override fun setBubbleLayout(holder: H, bubbleStyle: String, bubbleStyleSetting: String, reverseBubble: Boolean) {
|
||||
super.setBubbleLayout(holder, bubbleStyle, bubbleStyleSetting, reverseBubble)
|
||||
|
||||
//val bubbleView = holder.eventBaseView
|
||||
val bubbleView = holder.bubbleView
|
||||
val contentInBubble = infoInBubbles(holder.memberNameView.context)
|
||||
|
||||
when (bubbleStyle) {
|
||||
BubbleThemeUtils.BUBBLE_STYLE_NONE -> {
|
||||
@ -272,7 +323,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
} else {
|
||||
bubbleView.setBackgroundResource(if (reverseBubble) R.drawable.msg_bubble2_outgoing else R.drawable.msg_bubble2_incoming)
|
||||
}
|
||||
var tintColor = ColorStateList(
|
||||
val tintColor = ColorStateList(
|
||||
arrayOf(intArrayOf(0)),
|
||||
intArrayOf(ThemeUtils.getColor(bubbleView.context,
|
||||
if (attributes.informationData.sentByMe) R.attr.sc_message_bg_outgoing else R.attr.sc_message_bg_incoming)
|
||||
@ -315,6 +366,62 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
|
||||
round(shortPadding * density).toInt()
|
||||
)
|
||||
}
|
||||
|
||||
if (contentInBubble) {
|
||||
val anonymousReadReceipt = BubbleThemeUtils.getVisibleAnonymousReadReceipts(holder.bubbleFootView.context,
|
||||
attributes.informationData.readReceiptAnonymous, attributes.informationData.sentByMe)
|
||||
|
||||
when (anonymousReadReceipt) {
|
||||
AnonymousReadReceipt.PROCESSING -> {
|
||||
holder.bubbleFooterReadReceipt.visibility = View.VISIBLE
|
||||
holder.bubbleFooterReadReceipt.setImageResource(R.drawable.ic_processing_msg)
|
||||
}
|
||||
else -> {
|
||||
holder.bubbleFooterReadReceipt.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
if (allowFooterOverlay(holder)) {
|
||||
(holder.bubbleFootView.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.ALIGN_BOTTOM, R.id.viewStubContainer)
|
||||
(holder.bubbleFootView.layoutParams as RelativeLayout.LayoutParams).removeRule(RelativeLayout.BELOW)
|
||||
if (needsFooterReservation(holder)) {
|
||||
// Calculate required footer space
|
||||
val timeWidth: Int
|
||||
val timeHeight: Int
|
||||
if (BubbleThemeUtils.getBubbleTimeLocation(holder.bubbleTimeView.context) == BubbleThemeUtils.BUBBLE_TIME_BOTTOM) {
|
||||
// Guess text width for name and time
|
||||
timeWidth = ceil(BubbleThemeUtils.guessTextWidth(holder.bubbleFooterTimeView, attributes.informationData.time.toString())).toInt() + holder.bubbleFooterTimeView.paddingLeft + holder.bubbleFooterTimeView.paddingRight
|
||||
timeHeight = ceil(holder.bubbleFooterTimeView.textSize).toInt() + holder.bubbleFooterTimeView.paddingTop + holder.bubbleFooterTimeView.paddingBottom
|
||||
} else {
|
||||
timeWidth = 0
|
||||
timeHeight = 0
|
||||
}
|
||||
val readReceiptWidth: Int
|
||||
val readReceiptHeight: Int
|
||||
if (anonymousReadReceipt == AnonymousReadReceipt.NONE) {
|
||||
readReceiptWidth = 0
|
||||
readReceiptHeight = 0
|
||||
} else {
|
||||
readReceiptWidth = holder.bubbleFooterReadReceipt.maxWidth + holder.bubbleFooterReadReceipt.paddingLeft + holder.bubbleFooterReadReceipt.paddingRight
|
||||
readReceiptHeight = holder.bubbleFooterReadReceipt.maxHeight + holder.bubbleFooterReadReceipt.paddingTop + holder.bubbleFooterReadReceipt.paddingBottom
|
||||
}
|
||||
|
||||
var footerWidth = timeWidth + readReceiptWidth
|
||||
var footerHeight = max(timeHeight, readReceiptHeight)
|
||||
// Reserve extra padding, if we do have actual content
|
||||
if (footerWidth > 0) {
|
||||
footerWidth += holder.bubbleFootView.paddingLeft + holder.bubbleFootView.paddingRight
|
||||
}
|
||||
if (footerHeight > 0) {
|
||||
footerHeight += holder.bubbleFootView.paddingTop + holder.bubbleFootView.paddingBottom
|
||||
}
|
||||
reserveFooterSpace(holder, footerWidth, footerHeight)
|
||||
}
|
||||
} else {
|
||||
(holder.bubbleFootView.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.BELOW, R.id.viewStubContainer)
|
||||
(holder.bubbleFootView.layoutParams as RelativeLayout.LayoutParams).removeRule(RelativeLayout.ALIGN_BOTTOM)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,11 +27,12 @@ import androidx.core.view.isVisible
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import kotlin.math.max
|
||||
import kotlin.math.round
|
||||
import im.vector.app.R
|
||||
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.themes.BubbleThemeUtils
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import kotlin.math.ceil
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||
@ -115,12 +116,10 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
|
||||
val superVal = super.getViewStubMinimumWidth(holder, contentInBubble, showInformation)
|
||||
|
||||
// Guess text width for name and time
|
||||
val paint = Paint()
|
||||
paint.textSize = holder.filenameView.textSize
|
||||
val density = holder.filenameView.resources.displayMetrics.density
|
||||
// On first call, holder.fileImageView.width is not initialized yet
|
||||
val imageWidth = holder.fileImageView.resources.getDimensionPixelSize(R.dimen.chat_avatar_size)
|
||||
val minimumWidthWithText = round(paint.measureText(filename.toString())).toInt() + imageWidth + 32*density.toInt()
|
||||
val minimumWidthWithText = ceil(BubbleThemeUtils.guessTextWidth(holder.filenameView, filename)).toInt() + imageWidth + 32*density.toInt()
|
||||
val absoluteMinimumWidth = imageWidth*3
|
||||
return max(max(absoluteMinimumWidth, minimumWidthWithText), superVal)
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ data class MessageInformationData(
|
||||
val readReceipts: List<ReadReceiptData> = emptyList(),
|
||||
val referencesInfoData: ReferencesInfoData? = null,
|
||||
val sentByMe : Boolean,
|
||||
val readReceiptAnonymous: AnonymousReadReceipt,
|
||||
val isDirect: Boolean,
|
||||
val e2eDecoration: E2EDecoration = E2EDecoration.NONE
|
||||
) : Parcelable {
|
||||
@ -85,4 +86,12 @@ enum class E2EDecoration {
|
||||
WARN_SENT_BY_UNKNOWN
|
||||
}
|
||||
|
||||
enum class AnonymousReadReceipt {
|
||||
NONE,
|
||||
PROCESSING,
|
||||
// For future use?
|
||||
//SENT,
|
||||
//READ
|
||||
}
|
||||
|
||||
fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
|
||||
|
@ -19,12 +19,12 @@ package im.vector.app.features.home.room.detail.timeline.item
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.text.method.MovementMethod
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import androidx.core.text.PrecomputedTextCompat
|
||||
import androidx.core.widget.TextViewCompat
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.ui.views.FooteredTextView
|
||||
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
|
||||
@ -76,7 +76,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
override fun getViewType() = STUB_ID
|
||||
|
||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
||||
val messageView by bind<AppCompatTextView>(R.id.messageTextView)
|
||||
val messageView by bind<FooteredTextView>(R.id.messageTextView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -86,4 +86,17 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
|
||||
override fun messageBubbleAllowed(context: Context): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun allowFooterOverlay(holder: Holder): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun needsFooterReservation(holder: Holder): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun reserveFooterSpace(holder: Holder, width: Int, height: Int) {
|
||||
holder.messageView.footerWidth = width
|
||||
holder.messageView.footerHeight = height
|
||||
}
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
|
||||
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"
|
||||
private const val SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY"
|
||||
const val SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY = "SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY"
|
||||
private const val SETTINGS_12_24_TIMESTAMPS_KEY = "SETTINGS_12_24_TIMESTAMPS_KEY"
|
||||
private const val SETTINGS_SHOW_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY"
|
||||
private const val SETTINGS_SHOW_REDACTED_KEY = "SETTINGS_SHOW_REDACTED_KEY"
|
||||
|
@ -27,6 +27,7 @@ import im.vector.app.R
|
||||
import im.vector.app.core.extensions.restart
|
||||
import im.vector.app.core.preference.VectorListPreference
|
||||
import im.vector.app.core.preference.VectorPreference
|
||||
import im.vector.app.core.preference.VectorSwitchPreference
|
||||
import im.vector.app.features.configuration.VectorConfiguration
|
||||
import im.vector.app.features.themes.BubbleThemeUtils
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
@ -40,6 +41,9 @@ class VectorSettingsPreferencesFragment @Inject constructor(
|
||||
override var titleRes = R.string.settings_preferences
|
||||
override val preferenceXmlRes = R.xml.vector_settings_preferences
|
||||
|
||||
private var bubbleTimeLocationPref: VectorListPreference? = null
|
||||
private var alwaysShowTimestampsPref: VectorSwitchPreference? = null
|
||||
|
||||
private val selectedLanguagePreference by lazy {
|
||||
findPreference<VectorPreference>(VectorPreferences.SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY)!!
|
||||
}
|
||||
@ -85,12 +89,31 @@ class VectorSettingsPreferencesFragment @Inject constructor(
|
||||
darkThemePref.parent?.removePreference(darkThemePref)
|
||||
}
|
||||
|
||||
findPreference<VectorListPreference>(BubbleThemeUtils.BUBBLE_STYLE_KEY)!!
|
||||
.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ ->
|
||||
val bubbleStylePreference = findPreference<VectorListPreference>(BubbleThemeUtils.BUBBLE_STYLE_KEY)
|
||||
bubbleStylePreference!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
BubbleThemeUtils.invalidateBubbleStyle()
|
||||
updateBubbleDependencies(
|
||||
bubbleStyle = newValue as String,
|
||||
bubbleTimeLocation = bubbleTimeLocationPref!!.value
|
||||
)
|
||||
true
|
||||
}
|
||||
|
||||
bubbleTimeLocationPref = findPreference<VectorListPreference>(BubbleThemeUtils.BUBBLE_TIME_LOCATION_KEY)
|
||||
alwaysShowTimestampsPref = findPreference<VectorSwitchPreference>(VectorPreferences.SETTINGS_ALWAYS_SHOW_TIMESTAMPS_KEY)
|
||||
bubbleTimeLocationPref!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
|
||||
BubbleThemeUtils.invalidateBubbleStyle()
|
||||
updateBubbleDependencies(
|
||||
bubbleStyle = bubbleStylePreference.value,
|
||||
bubbleTimeLocation = newValue as String
|
||||
)
|
||||
true
|
||||
}
|
||||
updateBubbleDependencies(
|
||||
bubbleStyle = bubbleStylePreference.value,
|
||||
bubbleTimeLocation = bubbleTimeLocationPref!!.value
|
||||
)
|
||||
|
||||
// Url preview
|
||||
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SHOW_URL_PREVIEW_KEY)!!.let {
|
||||
/*
|
||||
@ -202,4 +225,9 @@ class VectorSettingsPreferencesFragment @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBubbleDependencies(bubbleStyle: String, bubbleTimeLocation: String) {
|
||||
bubbleTimeLocationPref?.setEnabled(BubbleThemeUtils.isBubbleTimeLocationSettingAllowed(bubbleStyle))
|
||||
alwaysShowTimestampsPref?.setEnabled(!BubbleThemeUtils.forceAlwaysShowTimestamps(bubbleStyle, bubbleTimeLocation))
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,23 @@
|
||||
package im.vector.app.features.themes
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Paint
|
||||
import android.widget.TextView
|
||||
import androidx.preference.PreferenceManager
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AnonymousReadReceipt
|
||||
|
||||
/**
|
||||
* Util class for managing themes.
|
||||
*/
|
||||
object BubbleThemeUtils {
|
||||
const val BUBBLE_STYLE_KEY = "BUBBLE_STYLE_KEY"
|
||||
const val BUBBLE_TIME_LOCATION_KEY = "BUBBLE_TIME_LOCATION_KEY"
|
||||
|
||||
const val BUBBLE_STYLE_NONE = "none"
|
||||
const val BUBBLE_STYLE_START = "start"
|
||||
const val BUBBLE_STYLE_BOTH = "both"
|
||||
const val BUBBLE_TIME_TOP = "top"
|
||||
const val BUBBLE_TIME_BOTTOM = "bottom"
|
||||
|
||||
// Special case of BUBBLE_STYLE_BOTH, to allow non-bubble items align to the sender either way
|
||||
// (not meant for user setting, but internal use)
|
||||
@ -20,6 +26,7 @@ object BubbleThemeUtils {
|
||||
const val BUBBLE_STYLE_START_HIDDEN = "start_hidden"
|
||||
|
||||
private var mBubbleStyle: String = ""
|
||||
private var mBubbleTimeLocation: String = ""
|
||||
|
||||
fun getBubbleStyle(context: Context): String {
|
||||
if (mBubbleStyle == "") {
|
||||
@ -28,6 +35,26 @@ object BubbleThemeUtils {
|
||||
return mBubbleStyle
|
||||
}
|
||||
|
||||
fun getBubbleTimeLocation(context: Context): String {
|
||||
if (mBubbleTimeLocation == "") {
|
||||
mBubbleTimeLocation = PreferenceManager.getDefaultSharedPreferences(context).getString(BUBBLE_TIME_LOCATION_KEY, BUBBLE_TIME_BOTTOM)!!
|
||||
}
|
||||
if (!isBubbleTimeLocationSettingAllowed(context)) {
|
||||
return BUBBLE_TIME_TOP;
|
||||
}
|
||||
return mBubbleTimeLocation
|
||||
}
|
||||
|
||||
fun getVisibleAnonymousReadReceipts(context: Context, readReceipt: AnonymousReadReceipt, sentByMe: Boolean): AnonymousReadReceipt {
|
||||
// TODO
|
||||
if (false) android.util.Log.e("SCSCSC", " " + context)
|
||||
return if (sentByMe && (/*TODO setting*/ true || readReceipt == AnonymousReadReceipt.PROCESSING)) {
|
||||
readReceipt
|
||||
} else {
|
||||
AnonymousReadReceipt.NONE
|
||||
}
|
||||
}
|
||||
|
||||
fun drawsActualBubbles(bubbleStyle: String): Boolean {
|
||||
return bubbleStyle == BUBBLE_STYLE_START || bubbleStyle == BUBBLE_STYLE_BOTH
|
||||
}
|
||||
@ -38,5 +65,36 @@ object BubbleThemeUtils {
|
||||
|
||||
fun invalidateBubbleStyle() {
|
||||
mBubbleStyle = ""
|
||||
mBubbleTimeLocation = ""
|
||||
}
|
||||
|
||||
fun guessTextWidth(view: TextView): Float {
|
||||
return guessTextWidth(view, view.text)
|
||||
}
|
||||
|
||||
fun guessTextWidth(view: TextView, text: CharSequence): Float {
|
||||
return guessTextWidth(view.textSize, text);
|
||||
}
|
||||
|
||||
fun guessTextWidth(textSize: Float, text: CharSequence): Float {
|
||||
val paint = Paint()
|
||||
paint.textSize = textSize
|
||||
return paint.measureText(text.toString())
|
||||
}
|
||||
|
||||
fun forceAlwaysShowTimestamps(bubbleStyle: String, bubbleTimeLocation: String): Boolean {
|
||||
return isBubbleTimeLocationSettingAllowed(bubbleStyle) && bubbleTimeLocation == BUBBLE_TIME_BOTTOM
|
||||
}
|
||||
|
||||
fun isBubbleTimeLocationSettingAllowed(bubbleStyle: String): Boolean {
|
||||
return bubbleStyle == BUBBLE_STYLE_BOTH || bubbleStyle == BUBBLE_STYLE_BOTH_HIDDEN
|
||||
}
|
||||
|
||||
fun forceAlwaysShowTimestamps(context: Context): Boolean {
|
||||
return forceAlwaysShowTimestamps(getBubbleStyle(context), getBubbleTimeLocation(context))
|
||||
}
|
||||
|
||||
fun isBubbleTimeLocationSettingAllowed(context: Context): Boolean {
|
||||
return isBubbleTimeLocationSettingAllowed(getBubbleStyle(context))
|
||||
}
|
||||
}
|
||||
|
9
vector/src/main/res/drawable/ic_processing_msg.xml
Normal file
9
vector/src/main/res/drawable/ic_processing_msg.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="16dp"
|
||||
android:width="16dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" />
|
||||
</vector>
|
@ -84,8 +84,7 @@
|
||||
android:layout_below="@id/messageMemberNameView"
|
||||
android:layout_toEndOf="@id/messageStartGuideline"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
>
|
||||
android:layout_marginEnd="0dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/bubbleView"
|
||||
@ -199,6 +198,41 @@
|
||||
tools:text="@tools:sample/date/hhmm" />
|
||||
-->
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_alignEnd="@id/viewStubContainer"
|
||||
tools:layout_alignBottom="@id/viewStubContainer"
|
||||
android:paddingTop="4dp"
|
||||
android:id="@+id/bubbleFootView">
|
||||
<TextView
|
||||
android:id="@+id/bubbleFooterMessageTimeView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:maxLines="1"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="12sp"
|
||||
android:layout_gravity="bottom"
|
||||
tools:text="@tools:sample/date/hhmm" />
|
||||
<!-- We read maxWidth and maxHeight from code to guess footer size -->
|
||||
<ImageView
|
||||
android:id="@+id/bubbleFooterReadReceipt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxWidth="16dp"
|
||||
android:maxHeight="16dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:layout_gravity="bottom"
|
||||
app:tint="?riotx_text_secondary"
|
||||
tools:src="@drawable/ic_processing_msg" />
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</FrameLayout>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<im.vector.app.core.ui.views.WrapWidthTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<im.vector.app.core.ui.views.FooteredTextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/messageTextView"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -27,6 +27,9 @@
|
||||
<string name="bubble_style_none">Keine</string>
|
||||
<string name="bubble_style_start">Selbe Seite</string>
|
||||
<string name="bubble_style_both">Beide Seiten</string>
|
||||
<string name="bubble_time_location">Zeitstempel-Platzierung</string>
|
||||
<string name="bubble_time_location_top">Oben</string>
|
||||
<string name="bubble_time_location_bottom">Unten</string>
|
||||
|
||||
<string name="settings_unimportant_counter_badge">Zähle unwichtige Chat-Ereignisse</string>
|
||||
<string name="settings_unimportant_counter_badge_summary">Betrachte auch Chats ohne Benachrichtigung beim Zählen der ungelesenen Nachrichten pro Kategorie</string>
|
||||
|
@ -12,6 +12,15 @@
|
||||
<item>both</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="bubble_time_location_entries" translatable="false">
|
||||
<item>@string/bubble_time_location_top</item>
|
||||
<item>@string/bubble_time_location_bottom</item>
|
||||
</string-array>
|
||||
<string-array name="bubble_time_location_values" translatable="false">
|
||||
<item>top</item>
|
||||
<item>bottom</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="room_unread_kind_entries" translatable="false">
|
||||
<item>@string/settings_room_unread_kind_default</item>
|
||||
<item>@string/settings_room_unread_kind_content</item>
|
||||
|
@ -27,6 +27,9 @@
|
||||
<string name="bubble_style_none">None</string>
|
||||
<string name="bubble_style_start">Same side</string>
|
||||
<string name="bubble_style_both">Both sides</string>
|
||||
<string name="bubble_time_location">Timestamp location</string>
|
||||
<string name="bubble_time_location_top">Top</string>
|
||||
<string name="bubble_time_location_bottom">Bottom</string>
|
||||
|
||||
<string name="settings_unimportant_counter_badge">Count unimportant chat events</string>
|
||||
<string name="settings_unimportant_counter_badge_summary">Include chats without notifications in the category unread counter</string>
|
||||
|
@ -39,6 +39,15 @@
|
||||
android:title="@string/bubble_style"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<im.vector.app.core.preference.VectorListPreference
|
||||
android:key="BUBBLE_TIME_LOCATION_KEY"
|
||||
android:defaultValue="bottom"
|
||||
android:title="@string/bubble_time_location"
|
||||
android:entries="@array/bubble_time_location_entries"
|
||||
android:entryValues="@array/bubble_time_location_values"
|
||||
android:summary="%s"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<im.vector.app.core.preference.VectorPreference
|
||||
android:dialogTitle="@string/font_size"
|
||||
android:key="SETTINGS_INTERFACE_TEXT_SIZE_KEY"
|
||||
|
Loading…
x
Reference in New Issue
Block a user