Refresh media selection and sending flow with a shiny new UX.

This commit is contained in:
Alex Hart
2021-09-02 17:04:43 -03:00
committed by Greyson Parrelli
parent a940487611
commit 664d6475d9
195 changed files with 7075 additions and 4812 deletions

View File

@@ -205,8 +205,8 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
import org.thoughtcrime.securesms.maps.PlacePickerActivity;
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult;
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
import org.thoughtcrime.securesms.messagerequests.MessageRequestState;
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
@@ -741,7 +741,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
initializeSecurity(isSecureText, isDefaultSms);
break;
case MEDIA_SENDER:
MediaSendActivityResult result = data.getParcelableExtra(MediaSendActivity.EXTRA_RESULT);
MediaSendActivityResult result = MediaSendActivityResult.fromData(data);
if (!Objects.equals(result.getRecipientId(), recipient.getId())) {
Log.w(TAG, "Result's recipientId did not match ours! Result: " + result.getRecipientId() + ", Activity: " + recipient.getId());
@@ -1144,7 +1144,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
public void onAttachmentMediaClicked(@NonNull Media media) {
linkPreviewViewModel.onUserCancel();
startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
startActivityForResult(MediaSelectionActivity.editor(ConversationActivity.this, sendButton.getSelectedTransport(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed()), MEDIA_SENDER);
container.hideCurrentInput(composeText);
}
@@ -1152,7 +1152,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
public void onAttachmentSelectorClicked(@NonNull AttachmentKeyboardButton button) {
switch (button) {
case GALLERY:
AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport());
AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport(), inputPanel.getQuote().isPresent());
break;
case FILE:
AttachmentManager.selectDocument(this, PICK_DOCUMENT);
@@ -1607,7 +1607,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (!Util.isEmpty(mediaList)) {
Log.d(TAG, "Handling shared Media.");
Intent sendIntent = MediaSendActivity.buildEditorIntent(this, mediaList, recipient.get(), draftText, sendButton.getSelectedTransport());
Intent sendIntent = MediaSelectionActivity.editor(this, sendButton.getSelectedTransport(), mediaList, recipient.getId(), draftText);
startActivityForResult(sendIntent, MEDIA_SENDER);
return new SettableFuture<>(false);
}
@@ -2563,7 +2563,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
Media media = new Media(uri, mimeType, 0, width, height, 0, 0, borderless, videoGif, Optional.absent(), Optional.absent(), Optional.absent());
startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
startActivityForResult(MediaSelectionActivity.editor(ConversationActivity.this, sendButton.getSelectedTransport(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed()), MEDIA_SENDER);
return new SettableFuture<>(false);
} else {
return attachmentManager.setMedia(glideRequests, uri, mediaType, getCurrentMediaConstraints(), width, height);
@@ -3309,7 +3309,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull String contentType, @NonNull Uri uri, long size, boolean clearCompose) {
if (sendButton.getSelectedTransport().isSms()) {
Media media = new Media(uri, contentType, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, false, false, Optional.absent(), Optional.absent(), Optional.absent());
Intent intent = MediaSendActivity.buildEditorIntent(this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport());
Intent intent = MediaSelectionActivity.editor(this, sendButton.getSelectedTransport(), Collections.singletonList(media), recipient.getId(), composeText.getTextTrimmed());
startActivityForResult(intent, MEDIA_SENDER);
return;
}
@@ -3448,7 +3448,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
.onAllGranted(() -> {
composeText.clearFocus();
startActivityForResult(MediaSendActivity.buildCameraIntent(ConversationActivity.this, recipient.get(), sendButton.getSelectedTransport()), MEDIA_SENDER);
startActivityForResult(MediaSelectionActivity.camera(ConversationActivity.this, sendButton.getSelectedTransport(), recipient.getId(), inputPanel.getQuote().isPresent()), MEDIA_SENDER);
overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary);
})
.onAnyDenied(() -> Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())

View File

@@ -9,10 +9,12 @@ import android.view.ViewGroup
import android.view.animation.AnimationUtils
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.PluralsRes
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -45,6 +47,7 @@ import java.util.function.Consumer
private const val ARG_MULTISHARE_ARGS = "multiselect.forward.fragment.arg.multishare.args"
private const val ARG_CAN_SEND_TO_NON_PUSH = "multiselect.forward.fragment.arg.can.send.to.non.push"
private const val ARG_TITLE = "multiselect.forward.fragment.title"
private val TAG = Log.tag(MultiselectForwardFragment::class.java)
class MultiselectForwardFragment :
@@ -61,7 +64,8 @@ class MultiselectForwardFragment :
private lateinit var selectionFragment: ContactSelectionListFragment
private lateinit var contactFilterView: ContactFilterView
private lateinit var addMessage: EditText
private lateinit var callback: Callback
private var callback: Callback? = null
private var dismissibleDialog: SimpleProgressDialog.DismissibleDialog? = null
@@ -96,7 +100,7 @@ class MultiselectForwardFragment :
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
callback = requireNotNull(findListener())
callback = findListener()
disposables.bindTo(viewLifecycleOwner.lifecycle)
selectionFragment = childFragmentManager.findFragmentById(R.id.contact_selection_list_fragment) as ContactSelectionListFragment
@@ -117,12 +121,15 @@ class MultiselectForwardFragment :
}
}
val title: TextView = view.findViewById(R.id.title)
val container = view.parent.parent.parent as FrameLayout
val bottomBar = LayoutInflater.from(requireContext()).inflate(R.layout.multiselect_forward_fragment_bottom_bar, container, false)
val shareSelectionRecycler: RecyclerView = bottomBar.findViewById(R.id.selected_list)
val shareSelectionAdapter = ShareSelectionAdapter()
val sendButton: View = bottomBar.findViewById(R.id.share_confirm)
title.setText(requireArguments().getInt(ARG_TITLE))
addMessage = bottomBar.findViewById(R.id.add_message)
sendButton.setOnClickListener {
@@ -162,6 +169,7 @@ class MultiselectForwardFragment :
MultiselectForwardState.Stage.SomeFailed -> dismissAndShowToast(R.plurals.MultiselectForwardFragment_messages_sent)
MultiselectForwardState.Stage.AllFailed -> dismissAndShowToast(R.plurals.MultiselectForwardFragment_messages_failed_to_send)
MultiselectForwardState.Stage.Success -> dismissAndShowToast(R.plurals.MultiselectForwardFragment_messages_sent)
is MultiselectForwardState.Stage.SelectionConfirmed -> dismissWithResult(it.stage.recipients)
}
sendButton.isEnabled = it.stage == MultiselectForwardState.Stage.Selection
@@ -170,6 +178,8 @@ class MultiselectForwardFragment :
bottomBar.addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->
selectionFragment.setRecyclerViewPaddingBottom(bottom - top)
}
addMessage.visible = getMultiShareArgs().isNotEmpty()
}
override fun onResume() {
@@ -222,18 +232,30 @@ class MultiselectForwardFragment :
private fun dismissAndShowToast(@PluralsRes toastTextResId: Int) {
val argCount = getMessageCount()
callback.onFinishForwardAction()
callback?.onFinishForwardAction()
dismissibleDialog?.dismiss()
Toast.makeText(requireContext(), requireContext().resources.getQuantityString(toastTextResId, argCount), Toast.LENGTH_SHORT).show()
dismissAllowingStateLoss()
}
private fun dismissWithResult(recipientIds: List<RecipientId>) {
callback?.onFinishForwardAction()
dismissibleDialog?.dismiss()
setFragmentResult(
RESULT_SELECTION,
Bundle().apply {
putParcelableArrayList(RESULT_SELECTION_RECIPIENTS, ArrayList(recipientIds))
}
)
dismissAllowingStateLoss()
}
private fun getMessageCount(): Int = getMultiShareArgs().size + if (addMessage.text.isNotEmpty()) 1 else 0
private fun handleMessageExpired() {
dismissAllowingStateLoss()
callback.onFinishForwardAction()
callback?.onFinishForwardAction()
dismissibleDialog?.dismiss()
Toast.makeText(requireContext(), resources.getQuantityString(R.plurals.MultiselectForwardFragment__couldnt_forward_messages, getMultiShareArgs().size), Toast.LENGTH_LONG).show()
}
@@ -299,6 +321,10 @@ class MultiselectForwardFragment :
}
companion object {
const val RESULT_SELECTION = "result_selection"
const val RESULT_SELECTION_RECIPIENTS = "result_selection_recipients"
@JvmStatic
fun show(supportFragmentManager: FragmentManager, multiselectForwardFragmentArgs: MultiselectForwardFragmentArgs) {
val fragment = MultiselectForwardFragment()
@@ -306,6 +332,7 @@ class MultiselectForwardFragment :
fragment.arguments = Bundle().apply {
putParcelableArrayList(ARG_MULTISHARE_ARGS, ArrayList(multiselectForwardFragmentArgs.multiShareArgs))
putBoolean(ARG_CAN_SEND_TO_NON_PUSH, multiselectForwardFragmentArgs.canSendToNonPush)
putInt(ARG_TITLE, multiselectForwardFragmentArgs.title)
}
fragment.show(supportFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)

View File

@@ -1,10 +1,12 @@
package org.thoughtcrime.securesms.conversation.mutiselect.forward
import android.content.Context
import androidx.annotation.StringRes
import androidx.annotation.WorkerThread
import org.signal.core.util.StreamUtil
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect
@@ -16,9 +18,17 @@ import org.thoughtcrime.securesms.sharing.MultiShareArgs
import org.whispersystems.libsignal.util.guava.Optional
import java.util.function.Consumer
/**
* Arguments for the MultiselectForwardFragment.
*
* @param canSendToNonPush Whether non-push recipients will be displayed
* @param multiShareArgs The items to forward. If this is an empty list, the fragment owner will be sent back a selected list of contacts.
* @param title The title to display at the top of the sheet
*/
class MultiselectForwardFragmentArgs(
val canSendToNonPush: Boolean,
val multiShareArgs: List<MultiShareArgs>
val multiShareArgs: List<MultiShareArgs> = listOf(),
@StringRes val title: Int = R.string.MultiselectForwardFragment__forward_to
) {
companion object {

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.mutiselect.forward
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sharing.ShareContact
data class MultiselectForwardState(
@@ -16,5 +17,6 @@ data class MultiselectForwardState(
object SomeFailed : Stage()
object AllFailed : Stage()
object Success : Stage()
data class SelectionConfirmed(val recipients: List<RecipientId>) : Stage()
}
}

View File

@@ -68,16 +68,26 @@ class MultiselectForwardViewModel(
private fun performSend(additionalMessage: String) {
store.update { it.copy(stage = MultiselectForwardState.Stage.SendPending) }
repository.send(
additionalMessage = additionalMessage,
multiShareArgs = records,
shareContacts = store.state.selectedContacts,
MultiselectForwardRepository.MultiselectForwardResultHandlers(
onAllMessageSentSuccessfully = { store.update { it.copy(stage = MultiselectForwardState.Stage.Success) } },
onAllMessagesFailed = { store.update { it.copy(stage = MultiselectForwardState.Stage.AllFailed) } },
onSomeMessagesFailed = { store.update { it.copy(stage = MultiselectForwardState.Stage.SomeFailed) } }
if (records.isEmpty()) {
store.update { state ->
state.copy(
stage = MultiselectForwardState.Stage.SelectionConfirmed(
state.selectedContacts.filter { it.recipientId.isPresent }.map { it.recipientId.get() }.distinct()
)
)
}
} else {
repository.send(
additionalMessage = additionalMessage,
multiShareArgs = records,
shareContacts = store.state.selectedContacts,
MultiselectForwardRepository.MultiselectForwardResultHandlers(
onAllMessageSentSuccessfully = { store.update { it.copy(stage = MultiselectForwardState.Stage.Success) } },
onAllMessagesFailed = { store.update { it.copy(stage = MultiselectForwardState.Stage.AllFailed) } },
onSomeMessagesFailed = { store.update { it.copy(stage = MultiselectForwardState.Stage.SomeFailed) } }
)
)
)
}
}
class Factory(