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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

@ -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"

View File

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

View File

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

View File

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

View File

@ -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"