From 2aaeda6ca8a57713e702cc7641e712fba952ac95 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Mon, 15 May 2023 12:38:28 -0400 Subject: [PATCH] Add initial send support to CFv2. --- .../InsetAwareConstraintLayout.java | 5 + .../securesms/components/SendButton.kt | 2 +- .../conversation/ConversationIntents.java | 4 + .../conversation/v2/ConversationFragment.kt | 281 +++++++++++++++++- .../conversation/v2/ConversationRepository.kt | 64 ++++ .../conversation/v2/ConversationViewModel.kt | 30 ++ .../res/layout/v2_conversation_fragment.xml | 33 +- 7 files changed, 410 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.java b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.java index 6621a91c58..2b782ded04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.java @@ -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 = () -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/SendButton.kt b/app/src/main/java/org/thoughtcrime/securesms/components/SendButton.kt index 837926e95c..d7aafe4f95 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/SendButton.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/SendButton.kt @@ -216,7 +216,7 @@ class SendButton(context: Context, attributeSet: AttributeSet?) : AppCompatImage .show(items) } - interface SendTypeChangedListener { + fun interface SendTypeChangedListener { fun onSendTypeChanged(newType: MessageSendType, manuallySelected: Boolean) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java index 286462cbb9..2abb2e50a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 95b614fd25..a2dfc267c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -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 + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt index dc2bcd46bd..8e681bf47c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt @@ -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, + 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) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index d6b9da3ef7..d9b2db983d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -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, + 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, diff --git a/app/src/main/res/layout/v2_conversation_fragment.xml b/app/src/main/res/layout/v2_conversation_fragment.xml index 9fe5dd78c8..d0c7bfc98d 100644 --- a/app/src/main/res/layout/v2_conversation_fragment.xml +++ b/app/src/main/res/layout/v2_conversation_fragment.xml @@ -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" /> - + app:constraint_referenced_ids="conversation_input_panel,attachment_editor_stub" /> + + + app:layout_constraintBottom_toTopOf="@+id/conversation_input_space_left" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + +