diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt index 5ee995ec65..4da206c16d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivity.kt @@ -1,40 +1,101 @@ package org.thoughtcrime.securesms.conversation.v2 import android.content.Intent +import android.os.Bundle import android.view.MotionEvent +import android.view.Window import androidx.activity.viewModels -import androidx.fragment.app.Fragment -import org.thoughtcrime.securesms.components.FragmentWrapperActivity +import io.reactivex.rxjava3.subjects.PublishSubject +import io.reactivex.rxjava3.subjects.Subject +import org.thoughtcrime.securesms.PassphraseRequiredActivity +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent +import org.thoughtcrime.securesms.components.settings.app.subscription.StripeRepository import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner +import org.thoughtcrime.securesms.util.Debouncer import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme +import java.util.concurrent.TimeUnit /** * Wrapper activity for ConversationFragment. */ -class ConversationActivity : FragmentWrapperActivity(), VoiceNoteMediaControllerOwner { +class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, DonationPaymentComponent { + + companion object { + private const val STATE_WATERMARK = "share_data_watermark" + } private val theme = DynamicNoActionBarTheme() + private val transitionDebouncer: Debouncer = Debouncer(150, TimeUnit.MILLISECONDS) + private var shareDataTimestamp: Long = -1L + override val voiceNoteMediaController = VoiceNoteMediaController(this, true) + override val stripeRepository: StripeRepository by lazy { StripeRepository(this) } + override val googlePayResultPublisher: Subject = PublishSubject.create() + private val motionEventRelay: MotionEventRelay by viewModels() override fun onPreCreate() { theme.onCreate(this) } + override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { + supportPostponeEnterTransition() + transitionDebouncer.publish { supportStartPostponedEnterTransition() } + window.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS) + + if (savedInstanceState != null) { + shareDataTimestamp = savedInstanceState.getLong(STATE_WATERMARK, -1L) + } else if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) { + shareDataTimestamp = System.currentTimeMillis() + } + + setContentView(R.layout.fragment_container) + + if (savedInstanceState == null) { + replaceFragment() + } + } + override fun onResume() { super.onResume() theme.onResume(this) } - override fun getFragment(): Fragment = ConversationFragment().apply { - arguments = intent.extras + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putLong(STATE_WATERMARK, shareDataTimestamp) + } + + override fun onDestroy() { + super.onDestroy() + transitionDebouncer.clear() } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - error("ON NEW INTENT") + setIntent(intent) + replaceFragment() + } + + @Suppress("DEPRECATION") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data)) + } + + private fun replaceFragment() { + val fragment = ConversationFragment().apply { + arguments = intent.extras + } + + supportFragmentManager + .beginTransaction() + .replace(R.id.fragment_container, fragment) + .disallowAddToBackStack() + .commitNowAllowingStateLoss() } override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { 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 220c2fd4e2..6d3fb056d0 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 @@ -227,7 +227,6 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModelV2 import org.thoughtcrime.securesms.longmessage.LongMessageFragment import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory -import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory.create import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult @@ -436,6 +435,7 @@ class ConversationFragment : private lateinit var conversationItemDecorations: ConversationItemDecorations private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback private lateinit var typingIndicatorDecoration: TypingIndicatorDecoration + private lateinit var backPressedCallback: BackPressedDelegate private var animationsAllowed = false private var actionMode: ActionMode? = null @@ -773,6 +773,11 @@ class ConversationFragment : private fun doAfterFirstRender() { Log.d(TAG, "doAfterFirstRender") + activity?.supportStartPostponedEnterTransition() + + backPressedCallback = BackPressedDelegate() + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backPressedCallback) + attachmentManager = AttachmentManager(requireContext(), requireView(), AttachmentManagerListener()) EventBus.getDefault().registerForLifecycle(groupCallViewModel, viewLifecycleOwner) @@ -900,6 +905,7 @@ class ConversationFragment : disposables.add( draftViewModel .state + .distinctUntilChanged { previous, next -> previous.voiceNoteDraft == next.voiceNoteDraft } .subscribe { inputPanel.voiceNoteDraft = it.voiceNoteDraft updateToggleButtonState() @@ -1897,6 +1903,21 @@ class ConversationFragment : composeText.clearFocus() } + private inner class BackPressedDelegate : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + Log.d(TAG, "onBackPressed()") + if (reactionDelegate.isShowing) { + reactionDelegate.hide() + } else if (isSearchRequested) { + searchMenuItem?.collapseActionView() + } else if (args.conversationScreenType.isInBubble) { + requireActivity().onBackPressed() + } else { + requireActivity().finish() + } + } + } + //region Message action handling private fun handleReplyToMessage(conversationMessage: ConversationMessage) { @@ -2490,7 +2511,13 @@ class ConversationFragment : override fun goToMediaPreview(parent: ConversationItem, sharedElement: View, args: MediaIntentFactory.MediaPreviewArgs) { if (this@ConversationFragment.args.conversationScreenType.isInBubble) { - requireActivity().startActivity(create(requireActivity(), args.skipSharedElementTransition(true))) + val recipient = viewModel.recipientSnapshot ?: return + val intent = ConversationIntents.createBuilderSync(requireActivity(), recipient.id, viewModel.threadId) + .withStartingPosition(binding.conversationItemRecycler.getChildAdapterPosition(parent)) + .build() + + requireActivity().startActivity(intent) + requireActivity().startActivity(MediaIntentFactory.create(requireActivity(), args.skipSharedElementTransition(true))) return } @@ -2506,7 +2533,7 @@ class ConversationFragment : sharedElement.transitionName = MediaPreviewV2Activity.SHARED_ELEMENT_TRANSITION_NAME requireActivity().setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback()) val options = ActivityOptions.makeSceneTransitionAnimation(requireActivity(), sharedElement, MediaPreviewV2Activity.SHARED_ELEMENT_TRANSITION_NAME) - requireActivity().startActivity(create(requireActivity(), args), options.toBundle()) + requireActivity().startActivity(MediaIntentFactory.create(requireActivity(), args), options.toBundle()) } override fun onEditedIndicatorClicked(messageRecord: MessageRecord) { @@ -2655,13 +2682,13 @@ class ConversationFragment : } } ) - } else { - clearFocusedItem() - adapter.toggleSelection(item) - binding.conversationItemRecycler.invalidateItemDecorations() - - actionMode = (requireActivity() as AppCompatActivity).startSupportActionMode(actionModeCallback) } + } else { + clearFocusedItem() + adapter.toggleSelection(item) + binding.conversationItemRecycler.invalidateItemDecorations() + + actionMode = (requireActivity() as AppCompatActivity).startSupportActionMode(actionModeCallback) } } 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 0041d06bcc..e139f4f9c4 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 @@ -207,7 +207,7 @@ class ConversationRepository( identityRecordsState: IdentityRecordsState? ): Completable { val sendCompletable = Completable.create { emitter -> - if (body.isEmpty() && slideDeck?.containsMediaSlide() != true && preUploadResults.isEmpty()) { + if (body.isEmpty() && slideDeck?.containsMediaSlide() != true && preUploadResults.isEmpty() && contacts.isEmpty()) { emitter.onError(InvalidMessageException("Message is empty!")) return@create } 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 0336a66ef0..13bd684083 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 @@ -71,7 +71,7 @@ import kotlin.time.Duration * ConversationViewModel, which operates solely off of a thread id that never changes. */ class ConversationViewModel( - private val threadId: Long, + val threadId: Long, requestedStartingPosition: Int, private val repository: ConversationRepository, recipientRepository: ConversationRecipientRepository,