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:
SpiritCroc 2020-11-01 12:14:08 +01:00
parent 5853709c97
commit 8602f1345b
16 changed files with 404 additions and 41 deletions

View File

@ -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)
}
}

View File

@ -18,9 +18,11 @@
package im.vector.app.features.home.room.detail.timeline.helper 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.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.extensions.localDateTime 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.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData 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.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData
import im.vector.app.features.settings.VectorPreferences 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.extensions.orFalse
import org.matrix.android.sdk.api.session.Session 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.events.model.EventType
@ -49,7 +52,8 @@ import javax.inject.Inject
class MessageInformationDataFactory @Inject constructor(private val session: Session, class MessageInformationDataFactory @Inject constructor(private val session: Session,
private val roomSummaryHolder: RoomSummaryHolder, private val roomSummaryHolder: RoomSummaryHolder,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val vectorPreferences: VectorPreferences) { private val vectorPreferences: VectorPreferences,
private val context: Context) {
fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData { fun create(event: TimelineEvent, nextEvent: TimelineEvent?): MessageInformationData {
// Non nullability has been tested before // Non nullability has been tested before
@ -81,7 +85,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
avatarUrl = event.senderInfo.avatarUrl, avatarUrl = event.senderInfo.avatarUrl,
memberName = event.senderInfo.disambiguatedDisplayName, memberName = event.senderInfo.disambiguatedDisplayName,
showInformation = showInformation, showInformation = showInformation,
forceShowTimestamp = vectorPreferences.alwaysShowTimeStamps(), forceShowTimestamp = vectorPreferences.alwaysShowTimeStamps() || BubbleThemeUtils.forceAlwaysShowTimestamps(context),
orderedReactionList = event.annotations?.reactionsSummary orderedReactionList = event.annotations?.reactionsSummary
// ?.filter { isSingleEmoji(it.key) } // ?.filter { isSingleEmoji(it.key) }
?.map { ?.map {
@ -113,6 +117,16 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
ReferencesInfoData(verificationState) ReferencesInfoData(verificationState)
}, },
sentByMe = event.root.senderId == session.myUserId, 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, isDirect = roomSummaryHolder.roomSummary?.isDirect ?: false,
e2eDecoration = e2eDecoration e2eDecoration = e2eDecoration
) )

View File

@ -18,18 +18,16 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.content.Context import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Paint
import android.graphics.Typeface import android.graphics.Typeface
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import kotlin.math.max import kotlin.math.max
import kotlin.math.round 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.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.themes.BubbleThemeUtils import im.vector.app.features.themes.BubbleThemeUtils
import im.vector.app.features.themes.ThemeUtils 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 * 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 avatarImageView: ImageView?
val memberNameView: TextView? val memberNameView: TextView?
val timeView: TextView? var timeView: TextView?
val hiddenViews = ArrayList<View>() val hiddenViews = ArrayList<View>()
val invisibleViews = 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.timeView)
hiddenViews.add(holder.bubbleTimeView) 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 // Dual-side bubbles: hide own avatar, and all in direct chats
if ((!attributes.informationData.showInformation) || if ((!attributes.informationData.showInformation) ||
(contentInBubble && (attributes.informationData.sentByMe || attributes.informationData.isDirect))) { (contentInBubble && (attributes.informationData.sentByMe || attributes.informationData.isDirect))) {
@ -117,15 +134,6 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
avatarImageView = holder.avatarImageView 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 // Views available in upstream Element
avatarImageView?.layoutParams = avatarImageView?.layoutParams?.apply { avatarImageView?.layoutParams = avatarImageView?.layoutParams?.apply {
height = attributes.avatarSize height = attributes.avatarSize
@ -143,8 +151,23 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
avatarImageView?.setOnLongClickListener(attributes.itemLongClickListener) avatarImageView?.setOnLongClickListener(attributes.itemLongClickListener)
memberNameView?.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) 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) { 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 bubbleView by bind<View>(R.id.bubbleView)
val bubbleMemberNameView by bind<TextView>(R.id.bubbleMessageMemberNameView) val bubbleMemberNameView by bind<TextView>(R.id.bubbleMessageMemberNameView)
val bubbleTimeView by bind<TextView>(R.id.bubbleMessageTimeView) 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) 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 { open fun getViewStubMinimumWidth(holder: H, contentInBubble: Boolean, showInformation: Boolean): Int {
return if (contentInBubble && (attributes.informationData.showInformation || attributes.informationData.forceShowTimestamp)) { return if (contentInBubble) {
// Guess text width for name and time if (BubbleThemeUtils.getBubbleTimeLocation(holder.bubbleTimeView.context) == BubbleThemeUtils.BUBBLE_TIME_BOTTOM) {
val text = if (attributes.informationData.showInformation) { if (attributes.informationData.showInformation) {
holder.bubbleMemberNameView.text.toString() + " " + holder.bubbleTimeView.text.toString() // 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 { } 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 { } else {
0 0
} }
@ -243,11 +282,23 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
return round(96*density).toInt() 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) { override fun setBubbleLayout(holder: H, bubbleStyle: String, bubbleStyleSetting: String, reverseBubble: Boolean) {
super.setBubbleLayout(holder, bubbleStyle, bubbleStyleSetting, reverseBubble) super.setBubbleLayout(holder, bubbleStyle, bubbleStyleSetting, reverseBubble)
//val bubbleView = holder.eventBaseView //val bubbleView = holder.eventBaseView
val bubbleView = holder.bubbleView val bubbleView = holder.bubbleView
val contentInBubble = infoInBubbles(holder.memberNameView.context)
when (bubbleStyle) { when (bubbleStyle) {
BubbleThemeUtils.BUBBLE_STYLE_NONE -> { BubbleThemeUtils.BUBBLE_STYLE_NONE -> {
@ -272,7 +323,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : AbsBaseMessageItem<H>
} else { } else {
bubbleView.setBackgroundResource(if (reverseBubble) R.drawable.msg_bubble2_outgoing else R.drawable.msg_bubble2_incoming) bubbleView.setBackgroundResource(if (reverseBubble) R.drawable.msg_bubble2_outgoing else R.drawable.msg_bubble2_incoming)
} }
var tintColor = ColorStateList( val tintColor = ColorStateList(
arrayOf(intArrayOf(0)), arrayOf(intArrayOf(0)),
intArrayOf(ThemeUtils.getColor(bubbleView.context, intArrayOf(ThemeUtils.getColor(bubbleView.context,
if (attributes.informationData.sentByMe) R.attr.sc_message_bg_outgoing else R.attr.sc_message_bg_incoming) 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() 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)
}
}
} }
} }

View File

@ -27,11 +27,12 @@ import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import kotlin.math.max import kotlin.math.max
import kotlin.math.round
import im.vector.app.R 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.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.themes.BubbleThemeUtils
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import kotlin.math.ceil
@EpoxyModelClass(layout = R.layout.item_timeline_event_base) @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() { abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
@ -115,12 +116,10 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
val superVal = super.getViewStubMinimumWidth(holder, contentInBubble, showInformation) val superVal = super.getViewStubMinimumWidth(holder, contentInBubble, showInformation)
// Guess text width for name and time // Guess text width for name and time
val paint = Paint()
paint.textSize = holder.filenameView.textSize
val density = holder.filenameView.resources.displayMetrics.density val density = holder.filenameView.resources.displayMetrics.density
// On first call, holder.fileImageView.width is not initialized yet // On first call, holder.fileImageView.width is not initialized yet
val imageWidth = holder.fileImageView.resources.getDimensionPixelSize(R.dimen.chat_avatar_size) 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 val absoluteMinimumWidth = imageWidth*3
return max(max(absoluteMinimumWidth, minimumWidthWithText), superVal) return max(max(absoluteMinimumWidth, minimumWidthWithText), superVal)
} }

View File

@ -42,6 +42,7 @@ data class MessageInformationData(
val readReceipts: List<ReadReceiptData> = emptyList(), val readReceipts: List<ReadReceiptData> = emptyList(),
val referencesInfoData: ReferencesInfoData? = null, val referencesInfoData: ReferencesInfoData? = null,
val sentByMe : Boolean, val sentByMe : Boolean,
val readReceiptAnonymous: AnonymousReadReceipt,
val isDirect: Boolean, val isDirect: Boolean,
val e2eDecoration: E2EDecoration = E2EDecoration.NONE val e2eDecoration: E2EDecoration = E2EDecoration.NONE
) : Parcelable { ) : Parcelable {
@ -85,4 +86,12 @@ enum class E2EDecoration {
WARN_SENT_BY_UNKNOWN WARN_SENT_BY_UNKNOWN
} }
enum class AnonymousReadReceipt {
NONE,
PROCESSING,
// For future use?
//SENT,
//READ
}
fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)

View File

@ -19,12 +19,12 @@ package im.vector.app.features.home.room.detail.timeline.item
import android.content.Context import android.content.Context
import android.text.TextUtils import android.text.TextUtils
import android.text.method.MovementMethod import android.text.method.MovementMethod
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.text.PrecomputedTextCompat import androidx.core.text.PrecomputedTextCompat
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.ui.views.FooteredTextView
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
@EpoxyModelClass(layout = R.layout.item_timeline_event_base) @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
@ -76,7 +76,7 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
override fun getViewType() = STUB_ID override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) { class Holder : AbsMessageItem.Holder(STUB_ID) {
val messageView by bind<AppCompatTextView>(R.id.messageTextView) val messageView by bind<FooteredTextView>(R.id.messageTextView)
} }
companion object { companion object {
@ -86,4 +86,17 @@ abstract class MessageTextItem : AbsMessageItem<MessageTextItem.Holder>() {
override fun messageBubbleAllowed(context: Context): Boolean { override fun messageBubbleAllowed(context: Context): Boolean {
return true 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
}
} }

View File

@ -88,7 +88,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY" const val SETTINGS_SHOW_URL_PREVIEW_KEY = "SETTINGS_SHOW_URL_PREVIEW_KEY"
private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY" private const val SETTINGS_SEND_TYPING_NOTIF_KEY = "SETTINGS_SEND_TYPING_NOTIF_KEY"
private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY" private const val SETTINGS_ENABLE_MARKDOWN_KEY = "SETTINGS_ENABLE_MARKDOWN_KEY"
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_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_READ_RECEIPTS_KEY = "SETTINGS_SHOW_READ_RECEIPTS_KEY"
private const val SETTINGS_SHOW_REDACTED_KEY = "SETTINGS_SHOW_REDACTED_KEY" private const val SETTINGS_SHOW_REDACTED_KEY = "SETTINGS_SHOW_REDACTED_KEY"

View File

@ -27,6 +27,7 @@ import im.vector.app.R
import im.vector.app.core.extensions.restart import im.vector.app.core.extensions.restart
import im.vector.app.core.preference.VectorListPreference import im.vector.app.core.preference.VectorListPreference
import im.vector.app.core.preference.VectorPreference 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.configuration.VectorConfiguration
import im.vector.app.features.themes.BubbleThemeUtils import im.vector.app.features.themes.BubbleThemeUtils
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@ -40,6 +41,9 @@ class VectorSettingsPreferencesFragment @Inject constructor(
override var titleRes = R.string.settings_preferences override var titleRes = R.string.settings_preferences
override val preferenceXmlRes = R.xml.vector_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 { private val selectedLanguagePreference by lazy {
findPreference<VectorPreference>(VectorPreferences.SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY)!! findPreference<VectorPreference>(VectorPreferences.SETTINGS_INTERFACE_LANGUAGE_PREFERENCE_KEY)!!
} }
@ -85,12 +89,31 @@ class VectorSettingsPreferencesFragment @Inject constructor(
darkThemePref.parent?.removePreference(darkThemePref) darkThemePref.parent?.removePreference(darkThemePref)
} }
findPreference<VectorListPreference>(BubbleThemeUtils.BUBBLE_STYLE_KEY)!! val bubbleStylePreference = findPreference<VectorListPreference>(BubbleThemeUtils.BUBBLE_STYLE_KEY)
.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> bubbleStylePreference!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
BubbleThemeUtils.invalidateBubbleStyle() BubbleThemeUtils.invalidateBubbleStyle()
updateBubbleDependencies(
bubbleStyle = newValue as String,
bubbleTimeLocation = bubbleTimeLocationPref!!.value
)
true 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 // Url preview
findPreference<SwitchPreference>(VectorPreferences.SETTINGS_SHOW_URL_PREVIEW_KEY)!!.let { 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))
}
} }

View File

@ -1,17 +1,23 @@
package im.vector.app.features.themes package im.vector.app.features.themes
import android.content.Context import android.content.Context
import android.graphics.Paint
import android.widget.TextView
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import im.vector.app.features.home.room.detail.timeline.item.AnonymousReadReceipt
/** /**
* Util class for managing themes. * Util class for managing themes.
*/ */
object BubbleThemeUtils { object BubbleThemeUtils {
const val BUBBLE_STYLE_KEY = "BUBBLE_STYLE_KEY" 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_NONE = "none"
const val BUBBLE_STYLE_START = "start" const val BUBBLE_STYLE_START = "start"
const val BUBBLE_STYLE_BOTH = "both" 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 // 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) // (not meant for user setting, but internal use)
@ -20,6 +26,7 @@ object BubbleThemeUtils {
const val BUBBLE_STYLE_START_HIDDEN = "start_hidden" const val BUBBLE_STYLE_START_HIDDEN = "start_hidden"
private var mBubbleStyle: String = "" private var mBubbleStyle: String = ""
private var mBubbleTimeLocation: String = ""
fun getBubbleStyle(context: Context): String { fun getBubbleStyle(context: Context): String {
if (mBubbleStyle == "") { if (mBubbleStyle == "") {
@ -28,6 +35,26 @@ object BubbleThemeUtils {
return mBubbleStyle 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 { fun drawsActualBubbles(bubbleStyle: String): Boolean {
return bubbleStyle == BUBBLE_STYLE_START || bubbleStyle == BUBBLE_STYLE_BOTH return bubbleStyle == BUBBLE_STYLE_START || bubbleStyle == BUBBLE_STYLE_BOTH
} }
@ -38,5 +65,36 @@ object BubbleThemeUtils {
fun invalidateBubbleStyle() { fun invalidateBubbleStyle() {
mBubbleStyle = "" 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))
} }
} }

View 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>

View File

@ -84,8 +84,7 @@
android:layout_below="@id/messageMemberNameView" android:layout_below="@id/messageMemberNameView"
android:layout_toEndOf="@id/messageStartGuideline" android:layout_toEndOf="@id/messageStartGuideline"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
android:layout_marginEnd="0dp" android:layout_marginEnd="0dp">
>
<RelativeLayout <RelativeLayout
android:id="@+id/bubbleView" android:id="@+id/bubbleView"
@ -199,6 +198,41 @@
tools:text="@tools:sample/date/hhmm" /> 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> </RelativeLayout>
</FrameLayout> </FrameLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/messageTextView" android:id="@+id/messageTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -27,6 +27,9 @@
<string name="bubble_style_none">Keine</string> <string name="bubble_style_none">Keine</string>
<string name="bubble_style_start">Selbe Seite</string> <string name="bubble_style_start">Selbe Seite</string>
<string name="bubble_style_both">Beide Seiten</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">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> <string name="settings_unimportant_counter_badge_summary">Betrachte auch Chats ohne Benachrichtigung beim Zählen der ungelesenen Nachrichten pro Kategorie</string>

View File

@ -12,6 +12,15 @@
<item>both</item> <item>both</item>
</string-array> </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"> <string-array name="room_unread_kind_entries" translatable="false">
<item>@string/settings_room_unread_kind_default</item> <item>@string/settings_room_unread_kind_default</item>
<item>@string/settings_room_unread_kind_content</item> <item>@string/settings_room_unread_kind_content</item>

View File

@ -27,6 +27,9 @@
<string name="bubble_style_none">None</string> <string name="bubble_style_none">None</string>
<string name="bubble_style_start">Same side</string> <string name="bubble_style_start">Same side</string>
<string name="bubble_style_both">Both sides</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">Count unimportant chat events</string>
<string name="settings_unimportant_counter_badge_summary">Include chats without notifications in the category unread counter</string> <string name="settings_unimportant_counter_badge_summary">Include chats without notifications in the category unread counter</string>

View File

@ -39,6 +39,15 @@
android:title="@string/bubble_style" android:title="@string/bubble_style"
app:iconSpaceReserved="false" /> 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 <im.vector.app.core.preference.VectorPreference
android:dialogTitle="@string/font_size" android:dialogTitle="@string/font_size"
android:key="SETTINGS_INTERFACE_TEXT_SIZE_KEY" android:key="SETTINGS_INTERFACE_TEXT_SIZE_KEY"