Add new story send final screen.

This commit is contained in:
Alex Hart 2022-07-26 16:55:19 -03:00 committed by Cody Henthorne
parent 3c78d8619a
commit 87cb2d6bf8
18 changed files with 282 additions and 468 deletions

View file

@ -376,6 +376,11 @@
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".mediasend.v2.stories.StoriesMultiselectForwardActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".PassphraseChangeActivity"
android:label="@string/AndroidManifest__change_passphrase"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>

View file

@ -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<ActionItem> = 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) {

View file

@ -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"

View file

@ -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) {

View file

@ -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)

View file

@ -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)

View file

@ -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()
}

View file

@ -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<Uri> = 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<Args, List<ContactSearchKey.RecipientSearchKey>>() {
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<ContactSearchKey.RecipientSearchKey> {
return multiselectContract.parseResult(resultCode, intent)
}
}
@Parcelize
class Args(
val multiselectForwardFragmentArgs: MultiselectForwardFragmentArgs,
val previews: List<Uri>
) : Parcelable
}

View file

@ -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)

View file

@ -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<String> = BehaviorSubject.createDefault("")
private val disposables = CompositeDisposable()
private val internalThumbnail = MutableLiveData<Bitmap>()
val thumbnail: LiveData<Bitmap> = internalThumbnail
private val internalTypeface = MutableLiveData<Typeface>()
val state: LiveData<TextStoryPostCreationState> = 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<Uri> {
return repository.compressToBlob(bitmap)
}
override fun onCleared() {
disposables.clear()
thumbnail.value?.recycle()
}
fun saveToInstanceState(outState: Bundle) {

View file

@ -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<RecipientId> = bundle.getParcelableArrayList<RecipientId>(ChooseGroupStoryBottomSheet.RESULT_SET)?.toSet() ?: emptySet()
val keys: Set<ContactSearchKey.RecipientSearchKey.Story> = 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<ContactSearchKey.RecipientSearchKey>) {
send()
}
override fun onMessageResentAfterSafetyNumberChangeInBottomSheet() = error("Not supported here")
override fun onCanceled() {
viewModel.onSendCancelled()
}
}

View file

@ -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<Uri> {
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<ContactSearchKey>, textStoryPostCreationState: TextStoryPostCreationState, linkPreview: LinkPreview?): Single<TextStoryPostSendResult> {
return UntrustedRecords
.checkForBadIdentityRecords(contactSearchKey.filterIsInstance(ContactSearchKey.RecipientSearchKey::class.java).toSet())

View file

@ -1,8 +0,0 @@
package org.thoughtcrime.securesms.mediasend.v2.text.send
enum class TextStoryPostSendState {
INIT,
SENDING,
SENT,
FAILED
}

View file

@ -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<List<IdentityRecord>>()
private val disposables = CompositeDisposable()
val state: LiveData<TextStoryPostSendState> = store.stateLiveData
val untrustedIdentities: Observable<List<IdentityRecord>> = untrustedIdentitySubject
override fun onCleared() {
disposables.clear()
}
fun onSending() {
store.update {
TextStoryPostSendState.SENDING
}
}
fun onSendCancelled() {
store.update {
TextStoryPostSendState.INIT
}
}
fun onSend(contactSearchKeys: Set<ContactSearchKey>, 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 <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(TextStoryPostSendViewModel(repository)) as T
}
}
}

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_arrow_left_24"
app:title="@string/conversation_activity__send" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/fragment_container_wrapper"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/signal_colorBackground"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<FrameLayout
android:id="@+id/preview_viewport"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="22.5dp"
app:layout_collapseMode="parallax">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/preview_media_2"
android:layout_width="110dp"
android:layout_height="177dp"
android:layout_gravity="center"
android:rotation="-15"
android:scaleType="centerCrop"
android:translationX="-28dp"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.WallpaperPreview"
tools:background="@color/red" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/preview_media_1"
android:layout_width="120dp"
android:layout_height="215dp"
android:layout_gravity="center"
android:padding="1.5dp"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.WallpaperPreview"
app:strokeColor="@color/signal_colorBackground"
app:strokeWidth="3dp"
tools:background="@color/green" />
</FrameLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View file

@ -1,118 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<org.thoughtcrime.securesms.util.views.DarkOverflowToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:navigationIcon="@drawable/ic_arrow_left_24"
app:title="@string/conversation_activity__send" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/core_grey_95"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
android:id="@+id/preview_viewport"
android:layout_width="match_parent"
android:layout_height="215dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="24dp"
android:importantForAccessibility="no"
android:scaleType="centerInside"
app:layout_collapseMode="parallax" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
android:id="@+id/search_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:background="@drawable/rounded_rectangle_secondary_18"
android:hint="@string/TextStoryPostSendFragment__search"
android:minHeight="44dp"
android:paddingHorizontal="16dp"
android:textAppearance="@style/Signal.Text.Body" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contacts_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="12dp"
android:layout_weight="1"
android:paddingBottom="44dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/contact_search_item" />
</LinearLayout>
<LinearLayout
android:id="@+id/list_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:alpha="0"
android:orientation="vertical"
android:translationY="48dp">
<View
android:id="@+id/divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/signal_divider_major"
android:translationY="48dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/selected_list"
android:layout_width="match_parent"
android:layout_height="44dp"
android:layout_marginEnd="16dp"
android:background="@color/signal_background_primary"
android:clipToPadding="false"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="78dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/share_contact_selection_item" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/share_confirm"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="bottom|end"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:contentDescription="@string/ShareActivity__share"
app:backgroundTint="@color/core_ultramarine"
app:srcCompat="@drawable/ic_send_24"
app:tint="@color/core_white" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>

View file

@ -112,4 +112,24 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.cardview.widget.CardView
android:id="@+id/send_in_progress_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:cardCornerRadius="18dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:indeterminate="true"
app:indicatorColor="@color/signal_colorPrimary" />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -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">
<action
android:id="@+id/action_textStoryPostCreationFragment_to_textStoryPostSendFragment"
app:destination="@id/textStoryPostSendFragment" />
</fragment>
<fragment
android:id="@+id/textStoryPostSendFragment"
android:name="org.thoughtcrime.securesms.mediasend.v2.text.send.TextStoryPostSendFragment"
android:label="text_story_post_send_fragment"
tools:layout="@layout/stories_send_text_post_fragment" />
tools:layout="@layout/stories_text_post_creation_fragment" />
<action
android:id="@+id/action_directly_to_mediaCaptureFragment"