From 87cb2d6bf8b0905dd94566a63ede22e58328dd2d Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Tue, 26 Jul 2022 16:55:19 -0300 Subject: [PATCH] Add new story send final screen. --- app/src/main/AndroidManifest.xml | 5 + .../contacts/paged/ContactSearchItems.kt | 14 +- .../forward/MultiselectForwardActivity.kt | 2 +- .../forward/MultiselectForwardFragment.kt | 1 + .../forward/MultiselectForwardFragmentArgs.kt | 3 +- .../mediasend/v2/MediaSelectionActivity.kt | 25 +- .../v2/review/MediaReviewFragment.kt | 23 +- .../StoriesMultiselectForwardActivity.kt | 92 ++++++++ .../v2/text/TextStoryPostCreationFragment.kt | 37 ++- .../v2/text/TextStoryPostCreationViewModel.kt | 10 +- .../v2/text/send/TextStoryPostSendFragment.kt | 213 ------------------ .../text/send/TextStoryPostSendRepository.kt | 14 ++ .../v2/text/send/TextStoryPostSendState.kt | 8 - .../text/send/TextStoryPostSendViewModel.kt | 77 ------- .../stories_multiselect_forward_activity.xml | 76 +++++++ .../stories_send_text_post_fragment.xml | 118 ---------- .../stories_text_post_creation_fragment.xml | 20 ++ app/src/main/res/navigation/media.xml | 12 +- 18 files changed, 282 insertions(+), 468 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/StoriesMultiselectForwardActivity.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendState.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendViewModel.kt create mode 100644 app/src/main/res/layout/stories_multiselect_forward_activity.xml delete mode 100644 app/src/main/res/layout/stories_send_text_post_fragment.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dccf2ab5ac..6ef3ec7cda 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -376,6 +376,11 @@ android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" /> + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt index 4c562592bf..5fa2537400 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt @@ -119,6 +119,14 @@ object ContactSearchItems { } } + override fun bindAvatar(model: StoryModel) { + if (model.story.recipient.isMyStory) { + avatar.setAvatarUsingProfile(Recipient.self()) + } else { + super.bindAvatar(model) + } + } + override fun bindLongPress(model: StoryModel) { itemView.setOnLongClickListener { val actions: List = when { @@ -216,14 +224,18 @@ object ContactSearchItems { } name.setText(getRecipient(model)) - avatar.setAvatar(getRecipient(model)) badge.setBadgeFromRecipient(getRecipient(model)) + bindAvatar(model) bindNumberField(model) bindLabelField(model) bindSmsTagField(model) } + protected open fun bindAvatar(model: T) { + avatar.setAvatar(getRecipient(model)) + } + protected open fun bindNumberField(model: T) { number.visible = getRecipient(model).isGroup if (getRecipient(model).isGroup) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt index b49c1918de..8cbfc72243 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardActivity.kt @@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.components.FragmentWrapperActivity import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment.Companion.RESULT_SELECTION -class MultiselectForwardActivity : FragmentWrapperActivity(), MultiselectForwardFragment.Callback { +open class MultiselectForwardActivity : FragmentWrapperActivity(), MultiselectForwardFragment.Callback, SearchConfigurationProvider { companion object { private const val ARGS = "args" diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt index 689abb544c..8079ea0430 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt @@ -119,6 +119,7 @@ class MultiselectForwardFragment : disposables.bindTo(viewLifecycleOwner.lifecycle) contactFilterView = view.findViewById(R.id.contact_filter_edit_text) + contactFilterView.visible = args.isSearchEnabled contactFilterView.setOnSearchInputFocusChangedListener { _, hasFocus -> if (hasFocus) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt index cb7f1f2993..1d34fa3f30 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragmentArgs.kt @@ -44,7 +44,8 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor( val forceSelectionOnly: Boolean = false, val selectSingleRecipient: Boolean = false, @ColorInt val sendButtonTint: Int = -1, - val storySendRequirements: Stories.MediaTransform.SendRequirements = Stories.MediaTransform.SendRequirements.CAN_NOT_SEND + val storySendRequirements: Stories.MediaTransform.SendRequirements = Stories.MediaTransform.SendRequirements.CAN_NOT_SEND, + val isSearchEnabled: Boolean = true ) : Parcelable { fun withSendButtonTint(@ColorInt sendButtonTint: Int) = copy(sendButtonTint = sendButtonTint) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt index afb97ad69c..614dd38305 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionActivity.kt @@ -13,7 +13,6 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProvider import androidx.navigation.Navigation import androidx.navigation.fragment.NavHostFragment @@ -25,11 +24,8 @@ import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.PassphraseRequiredActivity import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.emoji.EmojiEventListener -import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey -import org.thoughtcrime.securesms.contacts.paged.ContactSearchState import org.thoughtcrime.securesms.conversation.MessageSendType -import org.thoughtcrime.securesms.conversation.mutiselect.forward.SearchConfigurationProvider import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil @@ -49,8 +45,7 @@ class MediaSelectionActivity : MediaReviewFragment.Callback, EmojiKeyboardPageFragment.Callback, EmojiEventListener, - EmojiSearchFragment.Callback, - SearchConfigurationProvider { + EmojiSearchFragment.Callback { private var animateInShadowLayerValueAnimator: ValueAnimator? = null private var animateInTextColorValueAnimator: ValueAnimator? = null @@ -311,24 +306,6 @@ class MediaSelectionActivity : viewModel.sendCommand(HudCommand.CloseEmojiSearch) } - override fun getSearchConfiguration(fragmentManager: FragmentManager, contactSearchState: ContactSearchState): ContactSearchConfiguration? { - return if (isStory) { - ContactSearchConfiguration.build { - query = contactSearchState.query - - addSection( - ContactSearchConfiguration.Section.Stories( - groupStories = contactSearchState.groupStories, - includeHeader = true, - headerAction = Stories.getHeaderAction(fragmentManager) - ) - ) - } - } else { - null - } - } - private inner class OnBackPressed : OnBackPressedCallback(true) { override fun handleOnBackPressed() { val navController = Navigation.findNavController(this@MediaSelectionActivity, R.id.fragment_container) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt index 78c9050902..59ac994663 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/review/MediaReviewFragment.kt @@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionNavigator.Companion import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionState import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel import org.thoughtcrime.securesms.mediasend.v2.MediaValidator +import org.thoughtcrime.securesms.mediasend.v2.stories.StoriesMultiselectForwardActivity import org.thoughtcrime.securesms.mms.SentMediaQuality import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.util.MediaUtil @@ -137,7 +138,16 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) { sharedViewModel.sendCommand(HudCommand.SaveMedia) } - val recipientSelectionLauncher = registerForActivityResult(MultiselectForwardActivity.SelectionContract()) { keys -> + val multiselectContract = MultiselectForwardActivity.SelectionContract() + val storiesContract = StoriesMultiselectForwardActivity.SelectionContract() + + val multiselectLauncher = registerForActivityResult(multiselectContract) { keys -> + if (keys.isNotEmpty()) { + performSend(keys) + } + } + + val storiesLauncher = registerForActivityResult(storiesContract) { keys -> if (keys.isNotEmpty()) { performSend(keys) } @@ -148,9 +158,16 @@ class MediaReviewFragment : Fragment(R.layout.v2_media_review_fragment) { val args = MultiselectForwardFragmentArgs( false, title = R.string.MediaReviewFragment__send_to, - storySendRequirements = sharedViewModel.getStorySendRequirements() + storySendRequirements = sharedViewModel.getStorySendRequirements(), + isSearchEnabled = !sharedViewModel.isStory() ) - recipientSelectionLauncher.launch(args) + + if (sharedViewModel.isStory()) { + val previews = sharedViewModel.state.value?.selectedMedia?.take(2)?.map { it.uri } ?: emptyList() + storiesLauncher.launch(StoriesMultiselectForwardActivity.Args(args, previews)) + } else { + multiselectLauncher.launch(args) + } } else { performSend() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/StoriesMultiselectForwardActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/StoriesMultiselectForwardActivity.kt new file mode 100644 index 0000000000..879e21b49e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/stories/StoriesMultiselectForwardActivity.kt @@ -0,0 +1,92 @@ +package org.thoughtcrime.securesms.mediasend.v2.stories + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Parcelable +import android.view.ViewGroup +import android.widget.ImageView +import androidx.activity.result.contract.ActivityResultContract +import androidx.fragment.app.FragmentManager +import com.bumptech.glide.Glide +import kotlinx.parcelize.Parcelize +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration +import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey +import org.thoughtcrime.securesms.contacts.paged.ContactSearchState +import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardActivity +import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader +import org.thoughtcrime.securesms.stories.Stories +import org.thoughtcrime.securesms.util.visible + +class StoriesMultiselectForwardActivity : MultiselectForwardActivity() { + + companion object { + private const val PREVIEW_MEDIA = "preview_media" + } + + override val contentViewId: Int = R.layout.stories_multiselect_forward_activity + + override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { + super.onCreate(savedInstanceState, ready) + + val preview1View: ImageView = findViewById(R.id.preview_media_1) + val preview2View: ImageView = findViewById(R.id.preview_media_2) + val previewMedia: List = intent.getParcelableArrayListExtra(PREVIEW_MEDIA)!! + + preview1View.visible = previewMedia.isNotEmpty() + preview2View.visible = previewMedia.size > 1 + + if (previewMedia.isNotEmpty()) { + Glide.with(this) + .load(DecryptableStreamUriLoader.DecryptableUri(previewMedia.first())) + .into(preview1View) + } + + if (previewMedia.size > 1) { + Glide.with(this).load(DecryptableStreamUriLoader.DecryptableUri(previewMedia[1])).into(preview2View) + } + } + + override fun getSearchConfiguration(fragmentManager: FragmentManager, contactSearchState: ContactSearchState): ContactSearchConfiguration? { + return ContactSearchConfiguration.build { + query = contactSearchState.query + + addSection( + ContactSearchConfiguration.Section.Stories( + groupStories = contactSearchState.groupStories, + includeHeader = true, + headerAction = Stories.getHeaderAction(fragmentManager) + ) + ) + } + } + + @Suppress("WrongViewCast") + override fun getContainer(): ViewGroup { + return findViewById(R.id.content) + } + + class SelectionContract : ActivityResultContract>() { + + private val multiselectContract = MultiselectForwardActivity.SelectionContract() + + override fun createIntent(context: Context, input: Args): Intent { + return multiselectContract.createIntent(context, input.multiselectForwardFragmentArgs) + .setClass(context, StoriesMultiselectForwardActivity::class.java) + .putExtra(PREVIEW_MEDIA, ArrayList(input.previews)) + } + + override fun parseResult(resultCode: Int, intent: Intent?): List { + return multiselectContract.parseResult(resultCode, intent) + } + } + + @Parcelize + class Args( + val multiselectForwardFragmentArgs: MultiselectForwardFragmentArgs, + val previews: List + ) : Parcelable +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt index 71c2e2562d..2869a2f924 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt @@ -14,16 +14,19 @@ import androidx.navigation.fragment.findNavController import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey +import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.mediasend.v2.HudCommand import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel +import org.thoughtcrime.securesms.mediasend.v2.stories.StoriesMultiselectForwardActivity import org.thoughtcrime.securesms.mediasend.v2.text.send.TextStoryPostSendRepository import org.thoughtcrime.securesms.mediasend.v2.text.send.TextStoryPostSendResult import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet +import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.stories.StoryTextPostView import org.thoughtcrime.securesms.util.LifecycleDisposable -import org.thoughtcrime.securesms.util.navigation.safeNavigate +import org.thoughtcrime.securesms.util.visible class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creation_fragment), TextStoryPostTextEntryFragment.Callback, SafetyNumberBottomSheet.Callbacks { @@ -31,6 +34,7 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati private lateinit var backgroundButton: AppCompatImageView private lateinit var send: View private lateinit var storyTextPostView: StoryTextPostView + private lateinit var sendInProgressCard: View private val sharedViewModel: MediaSelectionViewModel by viewModels( ownerProducer = { @@ -65,6 +69,7 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati backgroundButton = view.findViewById(R.id.background_selector) send = view.findViewById(R.id.send) storyTextPostView = view.findViewById(R.id.story_text_post) + sendInProgressCard = view.findViewById(R.id.send_in_progress_indicator) val backgroundProtection: View = view.findViewById(R.id.background_protection) val addLinkProtection: View = view.findViewById(R.id.add_link_protection) @@ -120,7 +125,19 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati viewModel.setLinkPreview("") } + val launcher = registerForActivityResult(StoriesMultiselectForwardActivity.SelectionContract()) { + if (it.isNotEmpty()) { + performSend(it.toSet()) + } else { + send.isClickable = true + sendInProgressCard.visible = false + } + } + send.setOnClickListener { + send.isClickable = false + sendInProgressCard.visible = true + storyTextPostView.hideCloseButton() val contacts = (sharedViewModel.destination.getRecipientSearchKeyList() + sharedViewModel.destination.getRecipientSearchKey()) @@ -128,10 +145,20 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati .toSet() if (contacts.isEmpty()) { - viewModel.setBitmap(storyTextPostView.drawToBitmap()) - findNavController().safeNavigate(R.id.action_textStoryPostCreationFragment_to_textStoryPostSendFragment) + val bitmap = storyTextPostView.drawToBitmap() + viewModel.compressToBlob(bitmap).observeOn(AndroidSchedulers.mainThread()).subscribe { uri -> + launcher.launch( + StoriesMultiselectForwardActivity.Args( + MultiselectForwardFragmentArgs( + canSendToNonPush = false, + storySendRequirements = Stories.MediaTransform.SendRequirements.VALID_DURATION, + isSearchEnabled = false + ), + listOf(uri) + ) + ) + } } else { - send.isClickable = false performSend(contacts) } } @@ -166,6 +193,8 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati } is TextStoryPostSendResult.UntrustedRecordsError -> { send.isClickable = true + sendInProgressCard.visible = false + SafetyNumberBottomSheet .forIdentityRecordsAndDestinations(result.untrustedRecords, contacts.toList()) .show(childFragmentManager) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationViewModel.kt index 63bc79d69e..9182772494 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationViewModel.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.mediasend.v2.text import android.graphics.Bitmap import android.graphics.Typeface +import android.net.Uri import android.os.Bundle import androidx.annotation.ColorInt import androidx.lifecycle.LiveData @@ -32,9 +33,6 @@ class TextStoryPostCreationViewModel(private val repository: TextStoryPostSendRe private val temporaryBodySubject: Subject = BehaviorSubject.createDefault("") private val disposables = CompositeDisposable() - private val internalThumbnail = MutableLiveData() - val thumbnail: LiveData = internalThumbnail - private val internalTypeface = MutableLiveData() val state: LiveData = store.stateLiveData @@ -55,14 +53,12 @@ class TextStoryPostCreationViewModel(private val repository: TextStoryPostSendRe } } - fun setBitmap(bitmap: Bitmap) { - internalThumbnail.value?.recycle() - internalThumbnail.value = bitmap + fun compressToBlob(bitmap: Bitmap): Single { + return repository.compressToBlob(bitmap) } override fun onCleared() { disposables.clear() - thumbnail.value?.recycle() } fun saveToInstanceState(outState: Bundle) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendFragment.kt deleted file mode 100644 index 2a6e6e214b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendFragment.kt +++ /dev/null @@ -1,213 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.text.send - -import android.os.Bundle -import android.view.View -import android.widget.EditText -import android.widget.ImageView -import android.widget.Toast -import androidx.appcompat.widget.Toolbar -import androidx.core.widget.doAfterTextChanged -import androidx.fragment.app.Fragment -import androidx.fragment.app.setFragmentResultListener -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.RecyclerView -import org.signal.core.util.DimensionUnit -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.WrapperDialogFragment -import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration -import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey -import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator -import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel -import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseGroupStoryBottomSheet -import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet -import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationViewModel -import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet -import org.thoughtcrime.securesms.sharing.ShareSelectionAdapter -import org.thoughtcrime.securesms.sharing.ShareSelectionMappingModel -import org.thoughtcrime.securesms.stories.Stories -import org.thoughtcrime.securesms.stories.settings.create.CreateStoryFlowDialogFragment -import org.thoughtcrime.securesms.stories.settings.create.CreateStoryWithViewersFragment -import org.thoughtcrime.securesms.stories.settings.privacy.ChooseInitialMyStoryMembershipBottomSheetDialogFragment -import org.thoughtcrime.securesms.util.BottomSheetUtil -import org.thoughtcrime.securesms.util.FeatureFlags -import org.thoughtcrime.securesms.util.LifecycleDisposable -import org.thoughtcrime.securesms.util.livedata.LiveDataUtil - -class TextStoryPostSendFragment : - Fragment(R.layout.stories_send_text_post_fragment), - ChooseStoryTypeBottomSheet.Callback, - WrapperDialogFragment.WrapperDialogFragmentCallback, - ChooseInitialMyStoryMembershipBottomSheetDialogFragment.Callback, - SafetyNumberBottomSheet.Callbacks { - - private lateinit var shareListWrapper: View - private lateinit var shareSelectionRecyclerView: RecyclerView - private lateinit var shareConfirmButton: View - - private val shareSelectionAdapter = ShareSelectionAdapter() - private val disposables = LifecycleDisposable() - - private lateinit var contactSearchMediator: ContactSearchMediator - - private val viewModel: TextStoryPostSendViewModel by viewModels( - factoryProducer = { - TextStoryPostSendViewModel.Factory(TextStoryPostSendRepository()) - } - ) - - private val creationViewModel: TextStoryPostCreationViewModel by viewModels( - ownerProducer = { - requireActivity() - } - ) - - private val linkPreviewViewModel: LinkPreviewViewModel by viewModels( - ownerProducer = { - requireActivity() - } - ) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val toolbar: Toolbar = view.findViewById(R.id.toolbar) - val viewPort: ImageView = view.findViewById(R.id.preview_viewport) - val searchField: EditText = view.findViewById(R.id.search_field) - - toolbar.setNavigationOnClickListener { - findNavController().popBackStack() - } - - shareListWrapper = view.findViewById(R.id.list_wrapper) - shareConfirmButton = view.findViewById(R.id.share_confirm) - shareSelectionRecyclerView = view.findViewById(R.id.selected_list) - shareSelectionRecyclerView.adapter = shareSelectionAdapter - - disposables.bindTo(viewLifecycleOwner) - - creationViewModel.thumbnail.observe(viewLifecycleOwner) { bitmap -> - viewPort.setImageBitmap(bitmap) - } - - shareConfirmButton.setOnClickListener { - viewModel.onSending() - send() - } - - disposables += viewModel.untrustedIdentities.subscribe { records -> - SafetyNumberBottomSheet - .forIdentityRecordsAndDestinations(records, contactSearchMediator.getSelectedContacts().toList()) - .show(childFragmentManager) - } - - searchField.doAfterTextChanged { - contactSearchMediator.onFilterChanged(it?.toString()) - } - - setFragmentResultListener(CreateStoryWithViewersFragment.REQUEST_KEY) { _, bundle -> - val recipientId: RecipientId = bundle.getParcelable(CreateStoryWithViewersFragment.STORY_RECIPIENT)!! - contactSearchMediator.setKeysSelected(setOf(ContactSearchKey.RecipientSearchKey.Story(recipientId))) - contactSearchMediator.onFilterChanged("") - } - - setFragmentResultListener(ChooseGroupStoryBottomSheet.GROUP_STORY) { _, bundle -> - val groups: Set = bundle.getParcelableArrayList(ChooseGroupStoryBottomSheet.RESULT_SET)?.toSet() ?: emptySet() - val keys: Set = groups.map { ContactSearchKey.RecipientSearchKey.Story(it) }.toSet() - contactSearchMediator.addToVisibleGroupStories(keys) - contactSearchMediator.onFilterChanged("") - contactSearchMediator.setKeysSelected(keys) - } - - val contactsRecyclerView: RecyclerView = view.findViewById(R.id.contacts_container) - contactSearchMediator = ContactSearchMediator(this, contactsRecyclerView, FeatureFlags.shareSelectionLimit(), true, { contactSearchState -> - ContactSearchConfiguration.build { - query = contactSearchState.query - - addSection( - ContactSearchConfiguration.Section.Stories( - groupStories = contactSearchState.groupStories, - includeHeader = true, - headerAction = Stories.getHeaderAction(childFragmentManager) - ) - ) - } - }) - - contactSearchMediator.getSelectionState().observe(viewLifecycleOwner) { selection -> - shareSelectionAdapter.submitList(selection.mapIndexed { index, contact -> ShareSelectionMappingModel(contact.requireShareContact(), index == 0) }) - if (selection.isNotEmpty()) { - animateInSelection() - } else { - animateOutSelection() - } - } - - val saveStateAndSelection = LiveDataUtil.combineLatest(viewModel.state, contactSearchMediator.getSelectionState(), ::Pair) - saveStateAndSelection.observe(viewLifecycleOwner) { (state, selection) -> - when (state) { - TextStoryPostSendState.INIT -> shareConfirmButton.isEnabled = selection.isNotEmpty() - TextStoryPostSendState.SENDING -> shareConfirmButton.isEnabled = false - TextStoryPostSendState.SENT -> requireActivity().finish() - else -> { - Toast.makeText(requireContext(), R.string.TextStoryPostSendFragment__an_unexpected_error_occurred_try_again, Toast.LENGTH_SHORT).show() - viewModel.onSendCancelled() - } - } - } - } - - private fun send() { - shareConfirmButton.isEnabled = false - - val textStoryPostCreationState = creationViewModel.state.value - - viewModel.onSend( - contactSearchMediator.getSelectedContacts(), - textStoryPostCreationState!!, - linkPreviewViewModel.onSendWithErrorUrl().firstOrNull() - ) - } - - private fun animateInSelection() { - shareListWrapper.animate() - .alpha(1f) - .translationY(0f) - shareConfirmButton.animate() - .alpha(1f) - } - - private fun animateOutSelection() { - shareListWrapper.animate() - .alpha(0f) - .translationY(DimensionUnit.DP.toPixels(48f)) - shareConfirmButton.animate() - .alpha(0f) - } - - override fun onNewStoryClicked() { - CreateStoryFlowDialogFragment().show(parentFragmentManager, CreateStoryWithViewersFragment.REQUEST_KEY) - } - - override fun onGroupStoryClicked() { - ChooseGroupStoryBottomSheet().show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) - } - - override fun onWrapperDialogFragmentDismissed() { - contactSearchMediator.refresh() - } - - override fun onMyStoryConfigured(recipientId: RecipientId) { - contactSearchMediator.setKeysSelected(setOf(ContactSearchKey.RecipientSearchKey.Story(recipientId))) - contactSearchMediator.refresh() - } - - override fun sendAnywayAfterSafetyNumberChangedInBottomSheet(destinations: List) { - send() - } - - override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() = error("Not supported here") - - override fun onCanceled() { - viewModel.onSendCancelled() - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendRepository.kt index acbf5940f4..5292d53b10 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendRepository.kt @@ -1,6 +1,9 @@ package org.thoughtcrime.securesms.mediasend.v2.text.send +import android.graphics.Bitmap +import android.net.Uri import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.schedulers.Schedulers import org.signal.core.util.ThreadUtil import org.signal.core.util.logging.Log import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey @@ -16,14 +19,25 @@ import org.thoughtcrime.securesms.mediasend.v2.UntrustedRecords import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationState import org.thoughtcrime.securesms.mms.OutgoingMediaMessage import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage +import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.util.Base64 +import java.io.ByteArrayOutputStream private val TAG = Log.tag(TextStoryPostSendRepository::class.java) class TextStoryPostSendRepository { + fun compressToBlob(bitmap: Bitmap): Single { + return Single.fromCallable { + val outputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + bitmap.recycle() + BlobProvider.getInstance().forData(outputStream.toByteArray()).createForSingleUseInMemory() + }.subscribeOn(Schedulers.computation()) + } + fun send(contactSearchKey: Set, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?): Single { return UntrustedRecords .checkForBadIdentityRecords(contactSearchKey.filterIsInstance(ContactSearchKey.RecipientSearchKey::class.java).toSet()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendState.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendState.kt deleted file mode 100644 index da1aa7e0fb..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.text.send - -enum class TextStoryPostSendState { - INIT, - SENDING, - SENT, - FAILED -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendViewModel.kt deleted file mode 100644 index d9b224a035..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendViewModel.kt +++ /dev/null @@ -1,77 +0,0 @@ -package org.thoughtcrime.securesms.mediasend.v2.text.send - -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.plusAssign -import io.reactivex.rxjava3.kotlin.subscribeBy -import io.reactivex.rxjava3.subjects.PublishSubject -import org.signal.core.util.logging.Log -import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey -import org.thoughtcrime.securesms.database.model.IdentityRecord -import org.thoughtcrime.securesms.linkpreview.LinkPreview -import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationState -import org.thoughtcrime.securesms.util.livedata.Store - -private val TAG = Log.tag(TextStoryPostSendViewModel::class.java) - -class TextStoryPostSendViewModel(private val repository: TextStoryPostSendRepository) : ViewModel() { - - private val store = Store(TextStoryPostSendState.INIT) - private val untrustedIdentitySubject = PublishSubject.create>() - private val disposables = CompositeDisposable() - - val state: LiveData = store.stateLiveData - val untrustedIdentities: Observable> = untrustedIdentitySubject - - override fun onCleared() { - disposables.clear() - } - - fun onSending() { - store.update { - TextStoryPostSendState.SENDING - } - } - - fun onSendCancelled() { - store.update { - TextStoryPostSendState.INIT - } - } - - fun onSend(contactSearchKeys: Set, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?) { - store.update { - TextStoryPostSendState.SENDING - } - - disposables += repository.send(contactSearchKeys, textStoryPostCreationState, linkPreview).subscribeBy( - onSuccess = { - when (it) { - is TextStoryPostSendResult.Success -> { - store.update { TextStoryPostSendState.SENT } - } - is TextStoryPostSendResult.UntrustedRecordsError -> { - untrustedIdentitySubject.onNext(it.untrustedRecords) - store.update { TextStoryPostSendState.INIT } - } - is TextStoryPostSendResult.Failure -> { - store.update { TextStoryPostSendState.FAILED } - } - } - }, - onError = { - Log.w(TAG, "Unexpected error occurred", it) - store.update { TextStoryPostSendState.FAILED } - } - ) - } - - class Factory(private val repository: TextStoryPostSendRepository) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return modelClass.cast(TextStoryPostSendViewModel(repository)) as T - } - } -} diff --git a/app/src/main/res/layout/stories_multiselect_forward_activity.xml b/app/src/main/res/layout/stories_multiselect_forward_activity.xml new file mode 100644 index 0000000000..e8a68f60fc --- /dev/null +++ b/app/src/main/res/layout/stories_multiselect_forward_activity.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/stories_send_text_post_fragment.xml b/app/src/main/res/layout/stories_send_text_post_fragment.xml deleted file mode 100644 index 4958e361bd..0000000000 --- a/app/src/main/res/layout/stories_send_text_post_fragment.xml +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/stories_text_post_creation_fragment.xml b/app/src/main/res/layout/stories_text_post_creation_fragment.xml index d72e1091e6..1ae1743db8 100644 --- a/app/src/main/res/layout/stories_text_post_creation_fragment.xml +++ b/app/src/main/res/layout/stories_text_post_creation_fragment.xml @@ -112,4 +112,24 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/media.xml b/app/src/main/res/navigation/media.xml index 5854c2e687..2992440608 100644 --- a/app/src/main/res/navigation/media.xml +++ b/app/src/main/res/navigation/media.xml @@ -47,17 +47,7 @@ android:id="@+id/textStoryPostCreationFragment" android:name="org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationFragment" android:label="text_story_post_creation_fragment" - tools:layout="@layout/stories_text_post_creation_fragment"> - - - - + tools:layout="@layout/stories_text_post_creation_fragment" />