Add initial send support to CFv2.

This commit is contained in:
Cody Henthorne
2023-05-15 12:38:28 -04:00
committed by Greyson Parrelli
parent 5c78de2f46
commit 2aaeda6ca8
7 changed files with 410 additions and 9 deletions

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.util.AttributeSet;
import android.view.WindowInsets;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -76,6 +77,10 @@ public class InsetAwareConstraintLayout extends ConstraintLayout {
}
}
public void showSoftkey(@NonNull EditText editText) {
ViewUtil.focusAndShowKeyboard(editText);
}
public interface WindowInsetsTypeProvider {
WindowInsetsTypeProvider ALL = () ->

View File

@@ -216,7 +216,7 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage
.show(items)
}
interface SendTypeChangedListener {
fun interface SendTypeChangedListener {
fun onSendTypeChanged(newType: MessageSendType, manuallySelected: Boolean)
}

View File

@@ -411,6 +411,10 @@ public class ConversationIntents {
return Objects.equals(this, BUBBLE);
}
public boolean isInPopup() {
return Objects.equals(this, POPUP);
}
public boolean isNormal() {
return Objects.equals(this, NORMAL);
}

View File

@@ -8,11 +8,21 @@ package org.thoughtcrime.securesms.conversation.v2
import android.annotation.SuppressLint
import android.app.ActivityOptions
import android.content.Intent
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.View.OnFocusChangeListener
import android.view.inputmethod.EditorInfo
import android.widget.ImageButton
import android.widget.TextView
import android.widget.TextView.OnEditorActionListener
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.StringRes
@@ -30,6 +40,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.kotlin.subscribeBy
@@ -38,13 +49,21 @@ import org.greenrobot.eventbus.EventBus
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.signal.libsignal.protocol.InvalidMessageException
import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.gifts.flow.GiftFlowActivity
import org.thoughtcrime.securesms.badges.gifts.viewgift.received.ViewReceivedGiftBottomSheet
import org.thoughtcrime.securesms.badges.gifts.viewgift.sent.ViewSentGiftBottomSheet
import org.thoughtcrime.securesms.components.AnimatingToggle
import org.thoughtcrime.securesms.components.ComposeText
import org.thoughtcrime.securesms.components.HidingLinearLayout
import org.thoughtcrime.securesms.components.InputPanel
import org.thoughtcrime.securesms.components.InsetAwareConstraintLayout
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
import org.thoughtcrime.securesms.components.SendButton
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalFragment
@@ -62,6 +81,7 @@ import org.thoughtcrime.securesms.conversation.ConversationItem
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.ConversationOptionsMenu
import org.thoughtcrime.securesms.conversation.MarkReadHelper
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
@@ -93,6 +113,7 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescription
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
import org.thoughtcrime.securesms.invites.InviteActions
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.longmessage.LongMessageFragment
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
@@ -101,12 +122,14 @@ import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
import org.thoughtcrime.securesms.mms.AttachmentManager
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.notifications.v2.ConversationId
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientExporter
import org.thoughtcrime.securesms.recipients.RecipientFormattingException
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
@@ -181,6 +204,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
private lateinit var scrollToPositionDelegate: ScrollToPositionDelegate
private lateinit var adapter: ConversationAdapterV2
private lateinit var recyclerViewColorizer: RecyclerViewColorizer
private lateinit var attachmentManager: AttachmentManager
private var animationsAllowed = false
@@ -191,6 +215,21 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
}
}
private val container: InsetAwareConstraintLayout
get() = requireView() as InsetAwareConstraintLayout
private val inputPanel: InputPanel
get() = binding.conversationInputPanel.root
private val composeText: ComposeText
get() = binding.conversationInputPanel.embeddedTextEditor
private val sendButton: SendButton
get() = binding.conversationInputPanel.sendButton
private val sendEditButton: ImageButton
get() = binding.conversationInputPanel.sendEditButton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SignalLocalMetrics.ConversationOpen.start()
@@ -213,6 +252,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
)
conversationToolbarOnScrollHelper.attach(binding.conversationItemRecycler)
presentWallpaper(args.wallpaper)
presentChatColors(args.chatColors)
presentActionBarMenu()
observeConversationThread()
@@ -230,6 +270,11 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
}
}
override fun onPause() {
super.onPause()
ApplicationDependencies.getMessageNotifier().clearVisibleThread()
}
private fun observeConversationThread() {
var firstRender = true
disposables += viewModel
@@ -255,9 +300,9 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
scrollToPositionDelegate.notifyListCommitted()
if (firstRender) {
firstRender = false
binding.conversationItemRecycler.doAfterNextLayout {
SignalLocalMetrics.ConversationOpen.onRenderFinished()
firstRender = false
doAfterFirstRender()
animationsAllowed = true
}
@@ -268,6 +313,7 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
private fun doAfterFirstRender() {
Log.d(TAG, "doAfterFirstRender")
attachmentManager = AttachmentManager(requireContext(), requireView(), AttachmentManagerListener())
EventBus.getDefault().registerForLifecycle(groupCallViewModel, viewLifecycleOwner)
viewLifecycleOwner.lifecycle.addObserver(LastSeenPositionUpdater(adapter, layoutManager, viewModel))
@@ -291,6 +337,27 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
adapter.notifyItemRangeChanged(0, adapter.itemCount)
})
val sendButtonListener = SendButtonListener()
val composeTextEventsListener = ComposeTextEventsListener()
composeText.apply {
setOnEditorActionListener(sendButtonListener)
setCursorPositionChangedListener(composeTextEventsListener)
setOnKeyListener(composeTextEventsListener)
addTextChangedListener(composeTextEventsListener)
setOnClickListener(composeTextEventsListener)
onFocusChangeListener = composeTextEventsListener
setMessageSendType(MessageSendType.SignalMessageSendType)
}
sendButton.apply {
setOnClickListener(sendButtonListener)
isEnabled = true
post { sendButton.triggerSelectedChangedEvent() }
}
presentGroupCallJoinButton()
binding.scrollToBottom.setOnClickListener {
@@ -304,9 +371,23 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
adapter.registerAdapterDataObserver(DataObserver(scrollToPositionDelegate))
}
override fun onPause() {
super.onPause()
ApplicationDependencies.getMessageNotifier().clearVisibleThread()
private fun calculateCharactersRemaining() {
val messageBody: String = binding.conversationInputPanel.embeddedTextEditor.textTrimmed.toString()
val charactersLeftView: TextView = binding.conversationInputSpaceLeft
val characterState = MessageSendType.SignalMessageSendType.calculateCharacters(messageBody)
if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
charactersLeftView.text = String.format(
Locale.getDefault(),
"%d/%d (%d)",
characterState.charactersRemaining,
characterState.maxTotalMessageSize,
characterState.messagesSpent
)
charactersLeftView.visibility = View.VISIBLE
} else {
charactersLeftView.visibility = View.GONE
}
}
private fun registerForResults() {
@@ -376,6 +457,10 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
recyclerViewColorizer.setChatColors(chatColors)
binding.scrollToMention.setUnreadCountBackgroundTint(chatColors.asSingleColor())
binding.scrollToBottom.setUnreadCountBackgroundTint(chatColors.asSingleColor())
binding.conversationInputPanel.buttonToggle.background.apply {
colorFilter = PorterDuffColorFilter(chatColors.asSingleColor(), PorterDuff.Mode.MULTIPLY)
invalidateSelf()
}
}
private fun presentScrollButtons(scrollButtonState: ConversationScrollButtonState) {
@@ -504,6 +589,97 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
return callback
}
private fun updateToggleButtonState() {
val buttonToggle: AnimatingToggle = binding.conversationInputPanel.buttonToggle
val quickAttachment: HidingLinearLayout = binding.conversationInputPanel.quickAttachmentToggle
val inlineAttachment: HidingLinearLayout = binding.conversationInputPanel.inlineAttachmentContainer
when {
inputPanel.isRecordingInLockedMode -> {
buttonToggle.display(sendButton)
quickAttachment.show()
inlineAttachment.hide()
}
inputPanel.inEditMessageMode() -> {
buttonToggle.display(sendEditButton)
quickAttachment.hide()
inlineAttachment.hide()
}
// todo [cody] draftViewModel.voiceNoteDraft != null) { {
// buttonToggle.display(sendButton)
// quickAttachment.hide()
// inlineAttachment.hide()
// }
composeText.text?.isEmpty() == true && !attachmentManager.isAttachmentPresent -> {
buttonToggle.display(binding.conversationInputPanel.attachButton)
quickAttachment.show()
inlineAttachment.hide()
}
else -> {
buttonToggle.display(sendButton)
quickAttachment.hide()
if (!attachmentManager.isAttachmentPresent) { // todo [cody] && !linkPreviewViewModel.hasLinkPreviewUi()) {
inlineAttachment.show()
} else {
inlineAttachment.hide()
}
}
}
}
private fun sendMessage(metricId: String? = null, scheduledDate: Long = -1) {
val slideDeck: SlideDeck? = if (attachmentManager.isAttachmentPresent) attachmentManager.buildSlideDeck() else null
val send: Completable = viewModel.sendMessage(
metricId = metricId,
body = composeText.editableText.toString(),
slideDeck = slideDeck,
scheduledDate = scheduledDate,
messageToEdit = inputPanel.editMessageId,
quote = inputPanel.quote.orNull(),
mentions = composeText.mentions,
bodyRanges = composeText.styling
)
disposables += send
.doOnSubscribe {
composeText.setText("")
}
.subscribeBy(
onError = { t ->
Log.w(TAG, "Error sending", t)
when (t) {
is InvalidMessageException -> toast(R.string.ConversationActivity_message_is_empty_exclamation)
is RecipientFormattingException -> toast(R.string.ConversationActivity_recipient_is_not_a_valid_sms_or_email_address_exclamation, Toast.LENGTH_LONG)
}
},
onComplete = this::onSendComplete
)
}
private fun onSendComplete() {
if (isDetached || activity?.isFinishing == true) {
if (args.conversationScreenType.isInPopup) {
activity?.finish()
}
return
}
// todo [cody] fragment.setLastSeen(0);
scrollToPositionDelegate.resetScrollPosition()
attachmentManager.cleanup()
// todo [cody] updateLinkPreviewState();
// todo [cody] draftViewModel.onSendComplete(threadId);
inputPanel.exitEditMessageMode()
}
private fun toast(@StringRes toastTextId: Int, toastDuration: Int = Toast.LENGTH_SHORT) {
ThreadUtil.runOnMain {
if (context != null) {
@@ -514,6 +690,8 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
}
}
//region Scroll Handling
/**
* Requests a jump to the desired position, and ensures that the position desired will be visible on the screen.
*/
@@ -564,6 +742,10 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
}
}
//endregion Scroll Handling
// region Conversation Callbacks
private inner class ConversationItemClickListener : ConversationAdapter.ItemClickListener {
override fun onQuoteClicked(messageRecord: MmsMessageRecord) {
val quote: Quote? = messageRecord.quote
@@ -1005,6 +1187,8 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
}
}
// endregion Conversation Callbacks
private class LastSeenPositionUpdater(
val adapter: ConversationAdapterV2,
val layoutManager: LinearLayoutManager,
@@ -1022,4 +1206,93 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
viewModel.setLastScrolled(lastVisibleMessageTimestamp)
}
}
//region Compose + Send Callbacks
private inner class SendButtonListener : View.OnClickListener, OnEditorActionListener {
override fun onClick(v: View) {
val metricId = if (viewModel.recipientSnapshot?.isGroup == true) {
SignalLocalMetrics.GroupMessageSend.start()
} else {
SignalLocalMetrics.IndividualMessageSend.start()
}
sendMessage(metricId)
}
override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent): Boolean {
if (actionId == EditorInfo.IME_ACTION_SEND) {
if (inputPanel.isInEditMode) {
sendEditButton.performClick()
} else {
sendButton.performClick()
}
return true
}
return false
}
}
private inner class ComposeTextEventsListener :
View.OnKeyListener,
View.OnClickListener,
TextWatcher,
OnFocusChangeListener,
ComposeText.CursorPositionChangedListener {
private var beforeLength = 0
override fun onKey(v: View, keyCode: Int, event: KeyEvent): Boolean {
if (event.action == KeyEvent.ACTION_DOWN) {
if (keyCode == KeyEvent.KEYCODE_ENTER) {
if (SignalStore.settings().isEnterKeySends || event.isCtrlPressed) {
sendButton.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER))
sendButton.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER))
return true
}
}
}
return false
}
override fun onClick(v: View) {
container.showSoftkey(composeText)
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
beforeLength = composeText.textTrimmed.length
}
override fun afterTextChanged(s: Editable) {
calculateCharactersRemaining()
if (composeText.textTrimmed.isEmpty() || beforeLength == 0) {
composeText.postDelayed({ updateToggleButtonState() }, 50)
}
// todo [cody] stickerViewModel.onInputTextUpdated(s.toString())
}
override fun onFocusChange(v: View, hasFocus: Boolean) {
if (hasFocus) { // && container.getCurrentInput() == emojiDrawerStub.get()) {
container.showSoftkey(composeText)
}
}
override fun onCursorPositionChanged(start: Int, end: Int) {
// todo [cody] linkPreviewViewModel.onTextChanged(requireContext(), composeText.getTextTrimmed().toString(), start, end);
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit
}
//endregion Compose + Send Callbacks
private inner class AttachmentManagerListener : AttachmentManager.AttachmentListener {
override fun onAttachmentChanged() {
// TODO [cody] implement
}
override fun onLocationRemoved() {
// TODO [cody] implement
}
}
}

View File

@@ -6,11 +6,13 @@
package org.thoughtcrime.securesms.conversation.v2
import android.content.Context
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.libsignal.protocol.InvalidMessageException
import org.signal.paging.PagedData
import org.signal.paging.PagingConfig
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
@@ -18,11 +20,21 @@ import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.v2.data.ConversationDataSource
import org.thoughtcrime.securesms.database.RxDatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.Quote
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientFormattingException
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.util.SignalLocalMetrics
import kotlin.math.max
import kotlin.time.Duration.Companion.seconds
class ConversationRepository(context: Context) {
@@ -79,6 +91,58 @@ class ConversationRepository(context: Context) {
.subscribeOn(Schedulers.io())
}
fun sendMessage(
threadId: Long,
threadRecipient: Recipient?,
metricId: String?,
body: String,
slideDeck: SlideDeck?,
scheduledDate: Long,
messageToEdit: MessageId?,
quote: QuoteModel?,
mentions: List<Mention>,
bodyRanges: BodyRangeList?
): Completable {
val sendCompletable = Completable.create { emitter ->
if (body.isEmpty() && slideDeck?.containsMediaSlide() != true) {
emitter.onError(InvalidMessageException("Message is empty!"))
return@create
}
if (threadRecipient == null) {
emitter.onError(RecipientFormattingException("Badly formatted"))
return@create
}
val message = OutgoingMessage(
threadRecipient = threadRecipient,
sentTimeMillis = System.currentTimeMillis(),
body = body,
expiresIn = threadRecipient.expiresInSeconds.seconds.inWholeMilliseconds,
isUrgent = true,
isSecure = true,
bodyRanges = bodyRanges,
scheduledDate = scheduledDate,
outgoingQuote = quote,
messageToEdit = messageToEdit?.id ?: 0,
mentions = mentions
)
MessageSender.send(
ApplicationDependencies.getApplication(),
message,
threadId,
MessageSender.SendType.SIGNAL,
metricId
) {
emitter.onComplete()
}
}
return sendCompletable
.subscribeOn(Schedulers.io())
}
fun setLastVisibleMessageTimestamp(threadId: Long, lastVisibleMessageTimestamp: Long) {
SignalExecutors.BOUNDED.submit { SignalDatabase.threads.setLastScrolled(threadId, lastVisibleMessageTimestamp) }
}

View File

@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.conversation.v2
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
@@ -24,9 +25,14 @@ import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
import org.thoughtcrime.securesms.conversation.colors.NameColor
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.Quote
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.hasGiftBadge
@@ -159,6 +165,30 @@ class ConversationViewModel(
fun requestMarkRead(timestamp: Long) {
}
fun sendMessage(
metricId: String?,
body: String,
slideDeck: SlideDeck?,
scheduledDate: Long,
messageToEdit: MessageId?,
quote: QuoteModel?,
mentions: List<Mention>,
bodyRanges: BodyRangeList?
): Completable {
return repository.sendMessage(
threadId = threadId,
threadRecipient = recipientSnapshot,
metricId = metricId,
body = body,
slideDeck = slideDeck,
scheduledDate = scheduledDate,
messageToEdit = messageToEdit,
quote = quote,
mentions = mentions,
bodyRanges = bodyRanges
).observeOn(AndroidSchedulers.mainThread())
}
class Factory(
private val args: Args,
private val repository: ConversationRepository,

View File

@@ -43,8 +43,8 @@
android:overScrollMode="ifContentScrolls"
android:scrollbars="vertical"
android:splitMotionEvents="false"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/conversation_bottom_panel_barrier"
app:layout_constraintTop_toTopOf="parent"
tools:itemCount="20"
tools:listitem="@layout/conversation_item_sent_text_only" />
@@ -122,19 +122,44 @@
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/conversation_bottom_panel_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="conversation_input_panel" />
app:constraint_referenced_ids="conversation_input_panel,attachment_editor_stub" />
<ViewStub
android:id="@+id/attachment_editor_stub"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:inflatedId="@+id/attachment_editor"
android:layout="@layout/conversation_activity_attachment_editor_stub"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@+id/conversation_input_panel"/>
<include
android:id="@+id/conversation_input_panel"
layout="@layout/conversation_input_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@id/navigation_bar_guideline" />
app:layout_constraintBottom_toTopOf="@+id/conversation_input_space_left"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/conversation_input_space_left"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?android:windowBackground"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/navigation_bar_guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="160/160 (1)"
tools:visibility="visible" />
</org.thoughtcrime.securesms.components.InsetAwareConstraintLayout>