diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 20679cf618..27ba986214 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -160,7 +160,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe if (!isOxenHostedOpenGroup) { return } openGroupGuidelinesView.visibility = View.VISIBLE val recyclerViewLayoutParams = conversationRecyclerView.layoutParams as RelativeLayout.LayoutParams - recyclerViewLayoutParams.topMargin = toPx(57, resources) + recyclerViewLayoutParams.topMargin = toPx(57, resources) // The height of the open group guidelines view is hardcoded to this conversationRecyclerView.layoutParams = recyclerViewLayoutParams } @@ -397,6 +397,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } private fun isValidLockViewLocation(x: Int, y: Int): Boolean { + // We can be anywhere above the lock view and a bit to the side of it (at most `lockViewHitMargin` + // to the side) val lockViewLocation = IntArray(2) { 0 } lockView.getLocationOnScreen(lockViewLocation) val hitRect = Rect(lockViewLocation[0] - lockViewHitMargin, 0, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt index 0c583af299..e5bab6373e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -26,6 +26,7 @@ import kotlin.math.roundToInt class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate { private val screenWidth = Resources.getSystem().displayMetrics.widthPixels private val vMargin by lazy { toDp(4, resources) } + private val minHeight by lazy { toPx(56, resources) } var delegate: InputBarDelegate? = null var additionalContentHeight = 0 @@ -82,7 +83,7 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate { } override fun inputBarEditTextHeightChanged(newValue: Int) { - val newHeight = max(newValue + 2 * vMargin, toPx(56, resources)) + inputBarAdditionalContentContainer.height + val newHeight = max(newValue + 2 * vMargin, minHeight) + inputBarAdditionalContentContainer.height setHeight(newHeight) } @@ -100,18 +101,23 @@ class InputBar : RelativeLayout, InputBarEditTextDelegate, QuoteViewDelegate { quoteView.delegate = this inputBarAdditionalContentContainer.addView(quoteView) val attachments = (message as? MmsMessageRecord)?.slideDeck + // The max content width is the screen width - 2 times the horizontal input bar padding - the + // quote view content area's start and end margins. This unfortunately has to be calculated manually + // here to get the layout right. val maxContentWidth = (screenWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources) - toPx(30, resources)).roundToInt() quoteView.bind(message.individualRecipient.address.toString(), message.body, attachments, message.recipient, true, maxContentWidth, message.isOpenGroupInvitation) + // The 6 DP below is the padding the quote view applies to itself, which isn't included in the + // intrinsic height calculation. val quoteViewIntrinsicHeight = quoteView.getIntrinsicHeight(maxContentWidth) + toPx(6, resources) - val newHeight = max(inputBarEditText.height + 2 * vMargin, toPx(56, resources)) + quoteViewIntrinsicHeight + val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) + quoteViewIntrinsicHeight additionalContentHeight = quoteViewIntrinsicHeight setHeight(newHeight) } override fun cancelQuoteDraft() { inputBarAdditionalContentContainer.removeAllViews() - val newHeight = max(inputBarEditText.height + 2 * vMargin, toPx(56, resources)) + val newHeight = max(inputBarEditText.height + 2 * vMargin, minHeight) additionalContentHeight = 0 setHeight(newHeight) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt index 9fb480aeb9..f2d3e5eded 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarEditText.kt @@ -29,7 +29,8 @@ class InputBarEditText : AppCompatEditText { super.onTextChanged(text, start, lengthBefore, lengthAfter) delegate?.inputBarEditTextContentChanged(text) // Calculate the width manually to get it right even before layout has happened (i.e. - // when restoring a draft) + // when restoring a draft). The 64 DP is the horizontal margin around the input bar + // edit text. val width = (screenWidth - 2 * toPx(64.0f, resources)).roundToInt() if (width < 0) { return } // screenWidth initially evaluates to 0 val height = TextUtilities.getIntrinsicHeight(text, paint, width).toFloat() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt index 2b1a5cc7e2..43c4f17412 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/QuoteView.kt @@ -29,6 +29,13 @@ import kotlin.math.max import kotlin.math.min import kotlin.math.roundToInt +// There's quite some calculation going on here. It's a bit complex so don't make changes +// if you don't need to. If you do then test: +// • Quoted text in both private chats and group chats +// • Quoted images and videos in both private chats and group chats +// • Quoted voice messages and documents in both private chats and group chats +// • All of the above in both dark mode and light mode + class QuoteView : LinearLayout { private lateinit var mode: Mode private val vPadding by lazy { toPx(6, resources) } @@ -44,6 +51,8 @@ class QuoteView : LinearLayout { constructor(context: Context, mode: Mode) : super(context) { this.mode = mode LayoutInflater.from(context).inflate(R.layout.view_quote, this) + // Add padding here (not on mainQuoteViewContainer) to get a bit of a top inset while avoiding + // the clipping issue described in getIntrinsicHeight(maxContentWidth:). setPadding(0, toPx(6, resources), 0, 0) when (mode) { Mode.Draft -> quoteViewCancelButton.setOnClickListener { delegate?.cancelQuoteDraft() } @@ -51,6 +60,7 @@ class QuoteView : LinearLayout { quoteViewCancelButton.isVisible = false mainQuoteViewContainer.setBackgroundColor(ResourcesCompat.getColor(resources, R.color.transparent, context.theme)) val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams + // Since we're not showing the cancel button we can shorten the end margin quoteViewMainContentContainerLayoutParams.marginEnd = resources.getDimension(R.dimen.medium_spacing).roundToInt() quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams } @@ -60,6 +70,7 @@ class QuoteView : LinearLayout { // region General fun getIntrinsicContentHeight(maxContentWidth: Int): Int { + // If we're showing an attachment thumbnail, just constrain to the height of that if (quoteViewAttachmentPreviewContainer.isVisible) { return toPx(40, resources) } var result = 0 var authorTextViewIntrinsicHeight = 0 @@ -72,16 +83,24 @@ class QuoteView : LinearLayout { val bodyTextViewIntrinsicHeight = TextUtilities.getIntrinsicHeight(body, quoteViewBodyTextView.paint, maxContentWidth) result += bodyTextViewIntrinsicHeight if (!quoteViewAuthorTextView.isVisible) { + // We want to at least be as high as the cancel button, and no higher than 56 DP (that's + // approximately the height of 3 lines. return min(max(result, toPx(32, resources)), toPx(56, resources)) } else { + // Because we're showing the author text view, we should have a height of at least 32 DP + // anyway, so there's no need to constrain to that. We constrain to a max height of 56 DP + // because that's approximately the height of the author text view + 2 lines of the body + // text view. return min(result, toPx(56, resources)) } } fun getIntrinsicHeight(maxContentWidth: Int): Int { - var result = getIntrinsicContentHeight(maxContentWidth) - result += 2 * vPadding - return result + // The way all this works is that we just calculate the total height the quote view should be + // and then center everything inside vertically. This effectively means we're applying padding. + // Applying padding the regular way results in a clipping issue though due to a bug in + // RelativeLayout. + return getIntrinsicContentHeight(maxContentWidth) + 2 * vPadding } // endregion @@ -89,14 +108,16 @@ class QuoteView : LinearLayout { fun bind(authorPublicKey: String, body: String?, attachments: SlideDeck?, thread: Recipient, isOutgoingMessage: Boolean, maxContentWidth: Int, isOpenGroupInvitation: Boolean) { val contactDB = DatabaseFactory.getSessionContactDatabase(context) - quoteViewBodyTextView.maxLines = 3 + // Reduce the max body text view line count to 2 if this is a group thread because + // we'll be showing the author text view and we don't want the overall quote view height + // to get too big. + quoteViewBodyTextView.maxLines = if (thread.isGroupRecipient) 2 else 3 // Author if (thread.isGroupRecipient) { val author = contactDB.getContactWithSessionID(authorPublicKey) val authorDisplayName = author?.displayName(Contact.contextForRecipient(thread)) ?: authorPublicKey quoteViewAuthorTextView.text = authorDisplayName quoteViewAuthorTextView.setTextColor(getTextColor(isOutgoingMessage)) - quoteViewBodyTextView.maxLines = 2 } quoteViewAuthorTextView.isVisible = thread.isGroupRecipient // Body @@ -108,7 +129,7 @@ class QuoteView : LinearLayout { quoteViewAttachmentPreviewContainer.isVisible = hasAttachments if (!hasAttachments) { val accentLineLayoutParams = quoteViewAccentLine.layoutParams as RelativeLayout.LayoutParams - accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) + accentLineLayoutParams.height = getIntrinsicContentHeight(maxContentWidth) // Match the intrinsic * content * height quoteViewAccentLine.layoutParams = accentLineLayoutParams quoteViewAccentLine.setBackgroundColor(getLineColor(isOutgoingMessage)) } else { @@ -129,6 +150,7 @@ class QuoteView : LinearLayout { } mainQuoteViewContainer.layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, getIntrinsicHeight(maxContentWidth)) val quoteViewMainContentContainerLayoutParams = quoteViewMainContentContainer.layoutParams as RelativeLayout.LayoutParams + // The start margin is different if we just show the accent line vs if we show an attachment thumbnail quoteViewMainContentContainerLayoutParams.marginStart = if (!hasAttachments) toPx(16, resources) else toPx(48, resources) quoteViewMainContentContainer.layoutParams = quoteViewMainContentContainerLayoutParams } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index cbf9844bcf..1fd77eae43 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -65,6 +65,9 @@ class VisibleMessageContentView : LinearLayout { } else if (message is MmsMessageRecord && message.quote != null) { val quote = message.quote!! val quoteView = QuoteView(context, QuoteView.Mode.Regular) + // The max content width is the max message bubble size - 2 times the horizontal padding - the + // quote view content area's start margin. This unfortunately has to be calculated manually + // here to get the layout right. val maxContentWidth = (maxWidth - 2 * resources.getDimension(R.dimen.medium_spacing) - toPx(16, resources)).roundToInt() quoteView.bind(quote.author.toString(), quote.text, quote.attachment, thread, message.isOutgoing, maxContentWidth, message.isOpenGroupInvitation) @@ -76,6 +79,8 @@ class VisibleMessageContentView : LinearLayout { val voiceMessageView = VoiceMessageView(context) voiceMessageView.bind(message, background) mainContainer.addView(voiceMessageView) + // We have to use onContentClick (rather than a click listener directly on the voice + // message view) so as to not interfere with all the other gestures. onContentClick = { voiceMessageView.togglePlayback() } } else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) { val documentView = DocumentView(context) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 669716240a..3d665c5cbd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -81,7 +81,7 @@ class VisibleMessageView : LinearLayout { val threadDB = DatabaseFactory.getThreadDatabase(context) val thread = threadDB.getRecipientForThreadId(threadID)!! val contactDB = DatabaseFactory.getSessionContactDatabase(context) - val isGroupThread = (thread?.isGroupRecipient == true) + val isGroupThread = thread.isGroupRecipient val isStartOfMessageCluster = isStartOfMessageCluster(message, previous, isGroupThread) val isEndOfMessageCluster = isEndOfMessageCluster(message, next, isGroupThread) // Show profile picture and sender name if this is a group thread AND @@ -138,9 +138,10 @@ class VisibleMessageView : LinearLayout { } else { messageStatusImageView.isVisible = false } - // Populate content view + // Calculate max message bubble width var maxWidth = screenWidth - messageContentContainerLayoutParams.leftMargin - messageContentContainerLayoutParams.rightMargin if (profilePictureContainer.visibility != View.GONE) { maxWidth -= profilePictureContainer.width } + // Populate content view messageContentView.bind(message, isStartOfMessageCluster, isEndOfMessageCluster, glide, maxWidth, thread) }