diff --git a/app/src/main/kotlin/org/fossify/messages/activities/ThreadActivity.kt b/app/src/main/kotlin/org/fossify/messages/activities/ThreadActivity.kt index ec26de33d..6543db87f 100644 --- a/app/src/main/kotlin/org/fossify/messages/activities/ThreadActivity.kt +++ b/app/src/main/kotlin/org/fossify/messages/activities/ThreadActivity.kt @@ -159,6 +159,7 @@ import org.fossify.messages.extensions.updateScheduledMessagesThreadId import org.fossify.messages.helpers.CAPTURE_AUDIO_INTENT import org.fossify.messages.helpers.CAPTURE_PHOTO_INTENT import org.fossify.messages.helpers.CAPTURE_VIDEO_INTENT +import org.fossify.messages.helpers.EmojiReactionHelper import org.fossify.messages.helpers.FILE_SIZE_NONE import org.fossify.messages.helpers.IS_LAUNCHED_FROM_SHORTCUT import org.fossify.messages.helpers.IS_RECYCLE_BIN @@ -447,6 +448,7 @@ class ThreadActivity : SimpleActivity() { messages.removeAll { it.isScheduled && it.millis() < System.currentTimeMillis() } messages.sortBy { it.date } + messages = EmojiReactionHelper.applyEmojiReactions(messages) if (messages.size > MESSAGES_LIMIT) { messages = ArrayList(messages.takeLast(MESSAGES_LIMIT)) } diff --git a/app/src/main/kotlin/org/fossify/messages/adapters/ThreadAdapter.kt b/app/src/main/kotlin/org/fossify/messages/adapters/ThreadAdapter.kt index 1151bc5d6..9743714cc 100644 --- a/app/src/main/kotlin/org/fossify/messages/adapters/ThreadAdapter.kt +++ b/app/src/main/kotlin/org/fossify/messages/adapters/ThreadAdapter.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.graphics.Color import android.graphics.Typeface import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable import android.util.TypedValue import android.view.Menu import android.view.View @@ -13,6 +14,7 @@ import android.widget.LinearLayout import android.widget.RelativeLayout import androidx.appcompat.content.res.AppCompatResources import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toDrawable import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.DiffUtil @@ -35,10 +37,12 @@ import org.fossify.commons.extensions.beVisible import org.fossify.commons.extensions.beVisibleIf import org.fossify.commons.extensions.copyToClipboard import org.fossify.commons.extensions.formatDateOrTime +import org.fossify.commons.extensions.getBottomNavigationBackgroundColor import org.fossify.commons.extensions.getContrastColor import org.fossify.commons.extensions.getProperPrimaryColor import org.fossify.commons.extensions.getTextSize import org.fossify.commons.extensions.getTimeFormat +import org.fossify.commons.extensions.isDynamicTheme import org.fossify.commons.extensions.shareTextIntent import org.fossify.commons.extensions.showErrorToast import org.fossify.commons.extensions.usableScreenSize @@ -61,6 +65,7 @@ import org.fossify.messages.databinding.ItemThreadSendingBinding import org.fossify.messages.databinding.ItemThreadSuccessBinding import org.fossify.messages.dialogs.DeleteConfirmationDialog import org.fossify.messages.dialogs.MessageDetailsDialog +import org.fossify.messages.dialogs.ReactionDetailsDialog import org.fossify.messages.dialogs.SelectTextDialog import org.fossify.messages.extensions.config import org.fossify.messages.extensions.getContactFromAddress @@ -101,6 +106,9 @@ class ThreadAdapter( @SuppressLint("MissingPermission") private val hasMultipleSIMCards = (activity.subscriptionManagerCompat().activeSubscriptionInfoList?.size ?: 0) > 1 private val maxChatBubbleWidth = (activity.usableScreenSize.x * 0.8f).toInt() + private val reactionHorizontalOverlap = 8.dpToPx() + private val reactionVerticalOverlap = 4.dpToPx() + private val reactionElevation = 1.dpToPx() companion object { private const val MAX_MEDIA_HEIGHT_RATIO = 3 @@ -111,6 +119,7 @@ class ThreadAdapter( init { setupDragListener(true) setHasStableIds(true) + recyclerView.clipChildren = false (recyclerView.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false } @@ -385,6 +394,7 @@ class ThreadAdapter( } else { setupSentMessageView(messageBinding = this, message = message) } + setupEmojiReactions(messageBinding = this, message = message) if (message.attachment?.attachments?.isNotEmpty() == true) { threadMessageAttachmentsHolder.beVisible() @@ -406,6 +416,73 @@ class ThreadAdapter( } } + private fun setupEmojiReactions(messageBinding: ItemMessageBinding, message: Message) { + val reactions = message.emojiReactions + messageBinding.threadMessageReactions.apply { + beVisibleIf(reactions.isNotEmpty()) + if (reactions.isEmpty()) { + text = "" + setOnLongClickListener(null) + return + } + + val uniqueEmojis = reactions.map { it.emoji }.distinct() + text = if (reactions.size == 1) { + uniqueEmojis.first() + } else { + "${uniqueEmojis.joinToString("")}\u00A0${reactions.size}" + } + setOnLongClickListener { + showReactionDetails(message) + true + } + + if (message.isReceivedMessage()) { + background = createReactionBackground() + elevation = reactionElevation + translationX = reactionHorizontalOverlap + translationY = -reactionVerticalOverlap + setTextColor(textColor) + updateLayoutParams { + removeRule(RelativeLayout.END_OF) + removeRule(RelativeLayout.ALIGN_PARENT_END) + removeRule(RelativeLayout.ALIGN_END) + removeRule(RelativeLayout.ALIGN_RIGHT) + removeRule(RelativeLayout.ALIGN_START) + removeRule(RelativeLayout.ALIGN_LEFT) + addRule(RelativeLayout.ALIGN_END, messageBinding.threadMessageBody.id) + } + } else { + background = createReactionBackground() + elevation = reactionElevation + translationX = -reactionHorizontalOverlap + translationY = -reactionVerticalOverlap + setTextColor(textColor) + updateLayoutParams { + removeRule(RelativeLayout.END_OF) + removeRule(RelativeLayout.ALIGN_PARENT_END) + removeRule(RelativeLayout.ALIGN_END) + removeRule(RelativeLayout.ALIGN_RIGHT) + removeRule(RelativeLayout.ALIGN_LEFT) + addRule(RelativeLayout.ALIGN_START, messageBinding.threadMessageBody.id) + } + } + } + } + + private fun showReactionDetails(message: Message) { + val rows = message.emojiReactions.map { reaction -> + val contactName = message.participants + .firstOrNull { participant -> participant.doesHavePhoneNumber(reaction.senderPhoneNumber) } + ?.name + ?.takeIf { it.isNotBlank() } + ?: reaction.senderPhoneNumber + + "${reaction.emoji} $contactName" + } + ReactionDetailsDialog(activity, rows) + } + private fun setupReceivedMessageView(messageBinding: ItemMessageBinding, message: Message) { messageBinding.apply { with(ConstraintSet()) { @@ -635,6 +712,26 @@ class ThreadAdapter( } inner class ThreadViewHolder(val binding: ViewBinding) : ViewHolder(binding.root) + + private fun Int.dpToPx(): Float { + return this * resources.displayMetrics.density + } + + private fun createReactionBackground(): GradientDrawable { + return GradientDrawable().apply { + shape = GradientDrawable.RECTANGLE + cornerRadius = 18.dpToPx() + setColor(getBottomBarColor()) + } + } + + private fun getBottomBarColor(): Int { + return if (activity.isDynamicTheme()) { + ContextCompat.getColor(activity, org.fossify.commons.R.color.you_bottom_bar_color) + } else { + activity.getBottomNavigationBackgroundColor() + } + } } private class ThreadItemDiffCallback : DiffUtil.ItemCallback() { diff --git a/app/src/main/kotlin/org/fossify/messages/dialogs/ReactionDetailsDialog.kt b/app/src/main/kotlin/org/fossify/messages/dialogs/ReactionDetailsDialog.kt new file mode 100644 index 000000000..fdbdf6a70 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/messages/dialogs/ReactionDetailsDialog.kt @@ -0,0 +1,47 @@ +package org.fossify.messages.dialogs + +import android.view.ViewGroup +import org.fossify.commons.activities.BaseSimpleActivity +import org.fossify.commons.extensions.getAlertDialogBuilder +import org.fossify.commons.extensions.getProperTextColor +import org.fossify.commons.extensions.setupDialogStuff +import org.fossify.commons.views.MyTextView +import org.fossify.messages.R +import org.fossify.messages.databinding.DialogReactionDetailsBinding + +class ReactionDetailsDialog( + private val activity: BaseSimpleActivity, + reactions: List, +) { + init { + val binding = DialogReactionDetailsBinding.inflate(activity.layoutInflater).apply { + val rowPadding = 8.dpToPx() + reactions.forEach { reaction -> + dialogReactionDetailsHolder.addView( + MyTextView(activity).apply { + layoutParams = ViewGroup.MarginLayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + text = reaction + setTextColor(activity.getProperTextColor()) + textSize = REACTION_TEXT_SIZE + setPadding(0, rowPadding, 0, rowPadding) + } + ) + } + } + + activity.getAlertDialogBuilder().apply { + activity.setupDialogStuff(binding.root, this, R.string.reactions) + } + } + + private fun Int.dpToPx(): Int { + return (this * activity.resources.displayMetrics.density).toInt() + } + + private companion object { + const val REACTION_TEXT_SIZE = 22f + } +} diff --git a/app/src/main/kotlin/org/fossify/messages/extensions/Context.kt b/app/src/main/kotlin/org/fossify/messages/extensions/Context.kt index 8cf16f260..80a99b635 100644 --- a/app/src/main/kotlin/org/fossify/messages/extensions/Context.kt +++ b/app/src/main/kotlin/org/fossify/messages/extensions/Context.kt @@ -53,6 +53,7 @@ import org.fossify.messages.R import org.fossify.messages.databases.MessagesDatabase import org.fossify.messages.helpers.AttachmentUtils.parseAttachmentNames import org.fossify.messages.helpers.Config +import org.fossify.messages.helpers.EmojiReactionHelper import org.fossify.messages.helpers.FILE_SIZE_NONE import org.fossify.messages.helpers.MAX_MESSAGE_LENGTH import org.fossify.messages.helpers.MESSAGES_LIMIT @@ -205,7 +206,7 @@ fun Context.getMessages( } } - messages = messages + messages = EmojiReactionHelper.applyEmojiReactions(messages) .filter { it.participants.isNotEmpty() } .filterNot { it.isScheduled && it.millis() < System.currentTimeMillis() } .sortedWith(compareBy { it.date }.thenBy { it.id }) diff --git a/app/src/main/kotlin/org/fossify/messages/helpers/EmojiReactionHelper.kt b/app/src/main/kotlin/org/fossify/messages/helpers/EmojiReactionHelper.kt new file mode 100644 index 000000000..3a744c9fb --- /dev/null +++ b/app/src/main/kotlin/org/fossify/messages/helpers/EmojiReactionHelper.kt @@ -0,0 +1,158 @@ +@file:Suppress("MaxLineLength") + +package org.fossify.messages.helpers + +import org.fossify.messages.models.EmojiReaction +import org.fossify.messages.models.Message + +data class ParsedEmojiReaction( + val emoji: String, + val originalMessage: String, + val isRemoval: Boolean = false, +) + +object EmojiReactionHelper { + private val reactionPatterns: LinkedHashMap ParsedEmojiReaction?> = linkedMapOf( + Regex( + "(?s)^\u200a[^\u200b\u200a]*\u200b([^\u200b]*)\u200b[^\u200b\u200a]*\u200a(.*)\u200a[^\u200b\u200a]*\u200a\\Z" + ) to { match -> + ParsedEmojiReaction(match.groupValues[1], match.groupValues[2]) + } + ) + + private val removalPatterns: LinkedHashMap ParsedEmojiReaction?> = linkedMapOf( + Regex( + "(?s)^\u200a[^\u200c\u200a]*\u200c([^\u200c]*)\u200c[^\u200c\u200a]*\u200a(.*)\u200a[^\u200c\u200a]*\u200a\\Z" + ) to { match -> + ParsedEmojiReaction(match.groupValues[1], match.groupValues[2], isRemoval = true) + } + ) + + init { + addAppleTapbackPattern("❤️", "Loved", "Removed a heart from") + addAppleTapbackPattern("👍", "Liked", "Removed a like from") + addAppleTapbackPattern("👎", "Disliked", "Removed a dislike from") + addAppleTapbackPattern("😂", "Laughed at", "Removed a laugh from") + addAppleTapbackPattern("‼️", "Emphasized", "Removed an exclamation from") + addAppleTapbackPattern("❓", "Questioned", "Removed a question mark from") + + reactionPatterns[Regex("""(?s)^Reacted (.+?) to ["“](.+?)["”]$""")] = { match -> + if (match.groupValues.getOrNull(1) == "with a sticker") { + null + } else { + ParsedEmojiReaction(match.groupValues[1], match.groupValues[2]) + } + } + removalPatterns[Regex("""(?s)^Removed (.+?) from ["“](.+?)["”]$""")] = { match -> + ParsedEmojiReaction(match.groupValues[1], match.groupValues[2], isRemoval = true) + } + } + + fun parseEmojiReaction(body: String): ParsedEmojiReaction? { + parseRemoval(body)?.let { return it } + + return reactionPatterns.firstNotNullOfOrNull { (pattern, parser) -> + pattern.find(body)?.let { match -> parser(match) } + } + } + + fun applyEmojiReactions(messages: List): ArrayList { + messages.forEach { message -> + message.isEmojiReaction = false + message.emojiReactions = emptyList() + } + + val orderedMessages = messages.sortedWith(compareBy { it.date }.thenBy { it.id }) + orderedMessages.forEach { reactionMessage -> + val parsedReaction = parseEmojiReaction(reactionMessage.body) ?: return@forEach + val targetMessage = findTargetMessage( + messages = orderedMessages, + reactionMessage = reactionMessage, + originalMessageText = parsedReaction.originalMessage, + ) ?: return@forEach + + if (parsedReaction.isRemoval) { + removeEmojiReaction(reactionMessage, parsedReaction, targetMessage) + } else { + saveEmojiReaction(reactionMessage, parsedReaction, targetMessage) + } + } + + return orderedMessages + .filterNot { it.isEmojiReaction } + .toCollection(ArrayList()) + } + + private fun addAppleTapbackPattern(emoji: String, addedPrefix: String, removedPrefix: String) { + reactionPatterns[Regex("""(?s)^$addedPrefix ["“](.+?)["”]$""")] = { match -> + ParsedEmojiReaction(emoji, match.groupValues[1]) + } + removalPatterns[Regex("""(?s)^$removedPrefix ["“](.+?)["”]$""")] = { match -> + ParsedEmojiReaction(emoji, match.groupValues[1], isRemoval = true) + } + } + + private fun parseRemoval(body: String): ParsedEmojiReaction? { + return removalPatterns.firstNotNullOfOrNull { (pattern, parser) -> + pattern.find(body)?.let { match -> parser(match) } + } + } + + private fun findTargetMessage( + messages: List, + reactionMessage: Message, + originalMessageText: String, + ): Message? { + val originalMessageRegex = parseTruncatedMessage(originalMessageText) + return messages + .asReversed() + .firstOrNull { candidate -> + candidate.threadId == reactionMessage.threadId && + candidate.id != reactionMessage.id && + candidate.date <= reactionMessage.date && + !candidate.isEmojiReaction && + originalMessageRegex.matches(candidate.body.trim()) + } + } + + private fun parseTruncatedMessage(originalMessageText: String): Regex { + val reactionText = originalMessageText.trim() + val delimiter = "\u2026" + val index = reactionText.lastIndexOf(delimiter) + val regexPattern = if (index == -1) { + Regex.escape(reactionText) + } else { + val before = reactionText.take(index) + Regex.escape(before) + ".*" + } + return Regex("^$regexPattern$", RegexOption.DOT_MATCHES_ALL) + } + + private fun saveEmojiReaction( + reactionMessage: Message, + parsedReaction: ParsedEmojiReaction, + targetMessage: Message, + ) { + val reaction = EmojiReaction( + reactionMessageId = reactionMessage.id, + senderPhoneNumber = reactionMessage.senderPhoneNumber, + emoji = parsedReaction.emoji, + originalMessageText = parsedReaction.originalMessage, + ) + targetMessage.emojiReactions = targetMessage.emojiReactions + .filterNot { it.senderPhoneNumber == reaction.senderPhoneNumber } + reaction + reactionMessage.isEmojiReaction = true + } + + private fun removeEmojiReaction( + reactionMessage: Message, + parsedReaction: ParsedEmojiReaction, + targetMessage: Message, + ) { + targetMessage.emojiReactions = targetMessage.emojiReactions.filterNot { reaction -> + reaction.senderPhoneNumber == reactionMessage.senderPhoneNumber && + reaction.emoji == parsedReaction.emoji + } + reactionMessage.isEmojiReaction = true + } +} diff --git a/app/src/main/kotlin/org/fossify/messages/models/EmojiReaction.kt b/app/src/main/kotlin/org/fossify/messages/models/EmojiReaction.kt new file mode 100644 index 000000000..d59d291b6 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/messages/models/EmojiReaction.kt @@ -0,0 +1,8 @@ +package org.fossify.messages.models + +data class EmojiReaction( + val reactionMessageId: Long, + val senderPhoneNumber: String, + val emoji: String, + val originalMessageText: String, +) diff --git a/app/src/main/kotlin/org/fossify/messages/models/Message.kt b/app/src/main/kotlin/org/fossify/messages/models/Message.kt index 4ac987b85..d62c809da 100644 --- a/app/src/main/kotlin/org/fossify/messages/models/Message.kt +++ b/app/src/main/kotlin/org/fossify/messages/models/Message.kt @@ -3,6 +3,7 @@ package org.fossify.messages.models import android.provider.Telephony import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.Ignore import androidx.room.PrimaryKey import org.fossify.commons.models.SimpleContact import org.fossify.messages.helpers.THREAD_RECEIVED_MESSAGE @@ -27,6 +28,11 @@ data class Message( @ColumnInfo(name = "subscription_id") var subscriptionId: Int, @ColumnInfo(name = "is_scheduled") var isScheduled: Boolean = false ) : ThreadItem() { + @Ignore + var isEmojiReaction: Boolean = false + + @Ignore + var emojiReactions: List = emptyList() fun isReceivedMessage() = type == Telephony.Sms.MESSAGE_TYPE_INBOX @@ -62,7 +68,8 @@ data class Message( old.senderPhoneNumber == new.senderPhoneNumber && old.senderName == new.senderName && old.senderPhotoUri == new.senderPhotoUri && - old.isScheduled == new.isScheduled + old.isScheduled == new.isScheduled && + old.emojiReactions == new.emojiReactions } } } diff --git a/app/src/main/kotlin/org/fossify/messages/receivers/MmsReceiver.kt b/app/src/main/kotlin/org/fossify/messages/receivers/MmsReceiver.kt index 1d049965e..b9d8a2fca 100644 --- a/app/src/main/kotlin/org/fossify/messages/receivers/MmsReceiver.kt +++ b/app/src/main/kotlin/org/fossify/messages/receivers/MmsReceiver.kt @@ -14,11 +14,13 @@ import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.messages.R import org.fossify.messages.extensions.getConversations import org.fossify.messages.extensions.getLatestMMS +import org.fossify.messages.extensions.getMessages import org.fossify.messages.extensions.getNameFromAddress import org.fossify.messages.extensions.insertOrUpdateConversation import org.fossify.messages.extensions.shouldUnarchive import org.fossify.messages.extensions.showReceivedMessageNotification import org.fossify.messages.extensions.updateConversationArchivedStatus +import org.fossify.messages.helpers.EmojiReactionHelper import org.fossify.messages.helpers.ReceiverUtils.isMessageFilteredOut import org.fossify.messages.helpers.refreshConversations import org.fossify.messages.helpers.refreshMessages @@ -76,14 +78,21 @@ class MmsReceiver : MmsReceivedReceiver() { context.getNameFromAddress(address, it) } - context.showReceivedMessageNotification( - messageId = mms.id, - address = address, - senderName = senderName, - body = mms.body, - threadId = mms.threadId, - bitmap = glideBitmap - ) + val isVisibleMessage = EmojiReactionHelper.parseEmojiReaction(mms.body) == null || + context.getMessages(threadId = mms.threadId, includeScheduledMessages = false).any { + it.id == mms.id && it.isMMS + } + + if (isVisibleMessage) { + context.showReceivedMessageNotification( + messageId = mms.id, + address = address, + senderName = senderName, + body = mms.body, + threadId = mms.threadId, + bitmap = glideBitmap + ) + } val conversation = context.getConversations(mms.threadId).firstOrNull() ?: return runCatching { context.insertOrUpdateConversation(conversation) } diff --git a/app/src/main/kotlin/org/fossify/messages/receivers/SmsReceiver.kt b/app/src/main/kotlin/org/fossify/messages/receivers/SmsReceiver.kt index 15798c6c3..bc085fe65 100644 --- a/app/src/main/kotlin/org/fossify/messages/receivers/SmsReceiver.kt +++ b/app/src/main/kotlin/org/fossify/messages/receivers/SmsReceiver.kt @@ -13,6 +13,7 @@ import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.commons.models.PhoneNumber import org.fossify.commons.models.SimpleContact import org.fossify.messages.extensions.getConversations +import org.fossify.messages.extensions.getMessages import org.fossify.messages.extensions.getNameFromAddress import org.fossify.messages.extensions.getNotificationBitmap import org.fossify.messages.extensions.getThreadId @@ -23,6 +24,7 @@ import org.fossify.messages.extensions.shouldUnarchive import org.fossify.messages.extensions.showReceivedMessageNotification import org.fossify.messages.extensions.updateConversationArchivedStatus import org.fossify.messages.helpers.ReceiverUtils.isMessageFilteredOut +import org.fossify.messages.helpers.EmojiReactionHelper import org.fossify.messages.helpers.refreshConversations import org.fossify.messages.helpers.refreshMessages import org.fossify.messages.models.Message @@ -135,7 +137,14 @@ class SmsReceiver : BroadcastReceiver() { subscriptionId = subscriptionId ) - context.messagesDB.insertOrUpdate(message) + val isVisibleMessage = EmojiReactionHelper.parseEmojiReaction(body) == null || + context.getMessages(threadId = threadId, includeScheduledMessages = false).any { + it.id == newMessageId && !it.isMMS + } + + if (isVisibleMessage) { + context.messagesDB.insertOrUpdate(message) + } if (context.shouldUnarchive()) { context.updateConversationArchivedStatus(threadId, false) @@ -143,13 +152,15 @@ class SmsReceiver : BroadcastReceiver() { refreshMessages() refreshConversations() - context.showReceivedMessageNotification( - messageId = newMessageId, - address = address, - senderName = senderName, - body = body, - threadId = threadId, - bitmap = bitmap - ) + if (isVisibleMessage) { + context.showReceivedMessageNotification( + messageId = newMessageId, + address = address, + senderName = senderName, + body = body, + threadId = threadId, + bitmap = bitmap + ) + } } } diff --git a/app/src/main/res/layout/dialog_reaction_details.xml b/app/src/main/res/layout/dialog_reaction_details.xml new file mode 100644 index 000000000..e0689b272 --- /dev/null +++ b/app/src/main/res/layout/dialog_reaction_details.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml index a57c387f5..1cb45cad6 100644 --- a/app/src/main/res/layout/item_message.xml +++ b/app/src/main/res/layout/item_message.xml @@ -6,6 +6,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/small_margin" + android:clipChildren="false" + android:clipToPadding="false" android:foreground="@drawable/selector" android:paddingHorizontal="@dimen/activity_margin"> @@ -13,6 +15,8 @@ android:id="@+id/thread_message_wrapper" android:layout_width="0dp" android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -65,5 +69,20 @@ android:textSize="@dimen/normal_text_size" tools:drawableEndCompat="@drawable/scheduled_message_icon" tools:text="Message content" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 07213302a..a4799ff2a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ Receiver Sent at Received at + Reactions Received SMS New message