Implement video length enforcement for Stories.

This commit is contained in:
Alex Hart
2022-06-21 16:05:52 -03:00
committed by Cody Henthorne
parent 2c3d8337c3
commit 6a385c7a22
26 changed files with 597 additions and 108 deletions

View File

@@ -161,6 +161,7 @@ import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
import org.thoughtcrime.securesms.stories.Stories;
import org.thoughtcrime.securesms.stories.StoryViewerArgs;
import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity;
import org.thoughtcrime.securesms.util.CachedInflater;
@@ -1422,8 +1423,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
}
@Override
public boolean canSendMediaToStories() {
return true;
public @Nullable Stories.MediaTransform.SendRequirements getStorySendRequirements() {
return null;
}
@Override

View File

@@ -10,6 +10,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.fragments.findListener
class MultiselectForwardBottomSheet : FixedRoundedCornerBottomSheetDialogFragment(), MultiselectForwardFragment.Callback {
@@ -43,10 +44,9 @@ class MultiselectForwardBottomSheet : FixedRoundedCornerBottomSheetDialogFragmen
return backgroundColor
}
override fun canSendMediaToStories(): Boolean {
return findListener<Callback>()?.canSendMediaToStories() ?: true
override fun getStorySendRequirements(): Stories.MediaTransform.SendRequirements? {
return findListener<Callback>()?.getStorySendRequirements()
}
override fun setResult(bundle: Bundle) {
setFragmentResult(MultiselectForwardFragment.RESULT_KEY, bundle)
}
@@ -71,6 +71,6 @@ class MultiselectForwardBottomSheet : FixedRoundedCornerBottomSheetDialogFragmen
interface Callback {
fun onFinishForwardAction()
fun onDismissForwardSheet()
fun canSendMediaToStories(): Boolean = true
fun getStorySendRequirements(): Stories.MediaTransform.SendRequirements? = null
}
}

View File

@@ -14,6 +14,8 @@ import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.doOnNextLayout
import androidx.core.view.isVisible
@@ -26,6 +28,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ContactFilterView
import org.thoughtcrime.securesms.components.TooltipPopup
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.contacts.paged.ContactSearchMediator
@@ -80,6 +83,7 @@ class MultiselectForwardFragment :
private lateinit var contactFilterView: ContactFilterView
private lateinit var addMessage: EditText
private lateinit var contactSearchMediator: ContactSearchMediator
private lateinit var contactSearchRecycler: RecyclerView
private lateinit var callback: Callback
private var dismissibleDialog: SimpleProgressDialog.DismissibleDialog? = null
@@ -111,8 +115,8 @@ class MultiselectForwardFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.minimumHeight = resources.displayMetrics.heightPixels
val contactSearchRecycler: RecyclerView = view.findViewById(R.id.contact_selection_list)
contactSearchMediator = ContactSearchMediator(this, contactSearchRecycler, FeatureFlags.shareSelectionLimit(), !isSingleRecipientSelection(), this::getConfiguration)
contactSearchRecycler = view.findViewById(R.id.contact_selection_list)
contactSearchMediator = ContactSearchMediator(this, contactSearchRecycler, FeatureFlags.shareSelectionLimit(), !isSingleRecipientSelection(), this::getConfiguration, this::filterContacts)
callback = findListener()!!
disposables.bindTo(viewLifecycleOwner.lifecycle)
@@ -356,6 +360,51 @@ class MultiselectForwardFragment :
viewModel.cancelSend()
}
private fun getStorySendRequirements(): Stories.MediaTransform.SendRequirements {
return requireListener<Callback>().getStorySendRequirements() ?: viewModel.snapshot.storySendRequirements
}
private fun filterContacts(view: View?, contactSet: Set<ContactSearchKey>): Set<ContactSearchKey> {
val storySendRequirements = getStorySendRequirements()
val resultsSet = contactSet.filterNot {
it is ContactSearchKey.RecipientSearchKey && it.isStory && storySendRequirements == Stories.MediaTransform.SendRequirements.CAN_NOT_SEND
}
if (view != null && contactSet.any { it is ContactSearchKey.RecipientSearchKey && it.isStory }) {
@Suppress("NON_EXHAUSTIVE_WHEN_STATEMENT")
when (storySendRequirements) {
Stories.MediaTransform.SendRequirements.REQUIRES_CLIP -> {
if (!SignalStore.storyValues().videoTooltipSeen) {
displayTooltip(view, R.string.MultiselectForwardFragment__videos_will_be_trimmed) {
SignalStore.storyValues().videoTooltipSeen = true
}
}
}
Stories.MediaTransform.SendRequirements.CAN_NOT_SEND -> {
if (!SignalStore.storyValues().cannotSendTooltipSeen) {
displayTooltip(view, R.string.MultiselectForwardFragment__videos_sent_to_stories_cant) {
SignalStore.storyValues().cannotSendTooltipSeen = true
}
}
}
}
}
return resultsSet.toSet()
}
private fun displayTooltip(anchor: View, @StringRes text: Int, onDismiss: () -> Unit) {
TooltipPopup
.forTarget(anchor)
.setText(text)
.setTextColor(ContextCompat.getColor(requireContext(), R.color.signal_colorOnPrimary))
.setBackgroundTint(ContextCompat.getColor(requireContext(), R.color.signal_colorPrimary))
.setOnDismissListener {
onDismiss()
}
.show(TooltipPopup.POSITION_BELOW)
}
private fun getConfiguration(contactSearchState: ContactSearchState): ContactSearchConfiguration {
return findListener<SearchConfigurationProvider>()?.getSearchConfiguration(childFragmentManager, contactSearchState) ?: ContactSearchConfiguration.build {
query = contactSearchState.query
@@ -417,7 +466,7 @@ class MultiselectForwardFragment :
}
private fun isSelectedMediaValidForStories(): Boolean {
return requireListener<Callback>().canSendMediaToStories() && getMultiShareArgs().all { it.isValidForStories }
return getMultiShareArgs().all { it.isValidForStories }
}
private fun isSelectedMediaValidForNonStories(): Boolean {
@@ -439,7 +488,7 @@ class MultiselectForwardFragment :
fun setResult(bundle: Bundle)
fun getContainer(): ViewGroup
fun getDialogBackgroundColor(): Int
fun canSendMediaToStories(): Boolean = true
fun getStorySendRequirements(): Stories.MediaTransform.SendRequirements? = null
}
companion object {

View File

@@ -7,6 +7,7 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.setFragmentResult
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FullScreenDialogFragment
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.fragments.findListener
class MultiselectForwardFullScreenDialogFragment : FullScreenDialogFragment(), MultiselectForwardFragment.Callback {
@@ -33,6 +34,10 @@ class MultiselectForwardFullScreenDialogFragment : FullScreenDialogFragment(), M
return ContextCompat.getColor(requireContext(), R.color.signal_background_primary)
}
override fun getStorySendRequirements(): Stories.MediaTransform.SendRequirements? {
return findListener<Callback>()?.getStorySendRequirements()
}
override fun getContainer(): ViewGroup {
return requireView().findViewById(R.id.full_screen_dialog_content) as ViewGroup
}
@@ -47,12 +52,8 @@ class MultiselectForwardFullScreenDialogFragment : FullScreenDialogFragment(), M
override fun onSearchInputFocused() = Unit
override fun canSendMediaToStories(): Boolean {
return findListener<Callback>()?.canSendMediaToStories() ?: true
}
interface Callback {
fun onFinishForwardAction() = Unit
fun canSendMediaToStories(): Boolean = true
fun getStorySendRequirements(): Stories.MediaTransform.SendRequirements? = null
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.conversation.mutiselect.forward
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -8,6 +9,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sharing.MultiShareArgs
import org.thoughtcrime.securesms.sharing.MultiShareSender
import org.thoughtcrime.securesms.stories.Stories
import java.util.Optional
class MultiselectForwardRepository {
@@ -18,6 +20,20 @@ class MultiselectForwardRepository {
val onAllMessagesFailed: () -> Unit
)
fun checkAllSelectedMediaCanBeSentToStories(records: List<MultiShareArgs>): Single<Stories.MediaTransform.SendRequirements> {
if (!Stories.isFeatureEnabled() || records.isEmpty()) {
return Single.just(Stories.MediaTransform.SendRequirements.CAN_NOT_SEND)
}
return Single.fromCallable {
if (records.any { !it.isValidForStories }) {
Stories.MediaTransform.SendRequirements.CAN_NOT_SEND
} else {
Stories.MediaTransform.getSendRequirements(records.map { it.media }.flatten())
}
}.subscribeOn(Schedulers.io())
}
fun canSelectRecipient(recipientId: Optional<RecipientId>): Single<Boolean> {
if (!recipientId.isPresent) {
return Single.just(true)

View File

@@ -2,10 +2,13 @@ package org.thoughtcrime.securesms.conversation.mutiselect.forward
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.stories.Stories
data class MultiselectForwardState(
val stage: Stage = Stage.Selection
val stage: Stage = Stage.Selection,
val storySendRequirements: Stories.MediaTransform.SendRequirements = Stories.MediaTransform.SendRequirements.CAN_NOT_SEND
) {
sealed class Stage {
object Selection : Stage()
object FirstConfirmation : Stage()

View File

@@ -3,6 +3,8 @@ package org.thoughtcrime.securesms.conversation.mutiselect.forward
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mediasend.v2.UntrustedRecords
@@ -18,6 +20,19 @@ class MultiselectForwardViewModel(
private val store = Store(MultiselectForwardState())
val state: LiveData<MultiselectForwardState> = store.stateLiveData
val snapshot: MultiselectForwardState get() = store.state
private val disposables = CompositeDisposable()
init {
disposables += repository.checkAllSelectedMediaCanBeSentToStories(records).subscribe { sendRequirements ->
store.update { it.copy(storySendRequirements = sendRequirements) }
}
}
override fun onCleared() {
disposables.clear()
}
fun send(additionalMessage: String, selectedContacts: Set<ContactSearchKey>) {
if (SignalStore.tooltips().showMultiForwardDialog()) {