Add dialog protection and remote deletion to disabling stories and deleting lists.

This commit is contained in:
Alex Hart
2022-10-05 15:04:54 -03:00
committed by GitHub
parent ad1801108d
commit 4b94509a7a
15 changed files with 212 additions and 19 deletions

View File

@@ -0,0 +1,35 @@
package org.thoughtcrime.securesms.components
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
/**
* Manages the lifecycle of displaying a dialog fragment. Will automatically close and nullify the reference
* if the bound lifecycle is destroyed, and handles repeat calls to show such that no more than one dialog is
* displayed.
*/
class DialogFragmentDisplayManager(private val builder: () -> DialogFragment) : DefaultLifecycleObserver {
private var dialogFragment: DialogFragment? = null
fun show(lifecycleOwner: LifecycleOwner, fragmentManager: FragmentManager, tag: String? = null) {
val fragment = dialogFragment ?: builder()
if (fragment.dialog?.isShowing != true) {
fragment.show(fragmentManager, tag)
dialogFragment = fragment
lifecycleOwner.lifecycle.addObserver(this)
}
}
fun hide() {
dialogFragment?.dismissNow()
dialogFragment = null
}
override fun onDestroy(owner: LifecycleOwner) {
owner.lifecycle.removeObserver(this)
hide()
}
}

View File

@@ -0,0 +1,20 @@
package org.thoughtcrime.securesms.components
import android.app.Dialog
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import org.thoughtcrime.securesms.R
/**
* Displays a small progress spinner in a card view, as a non-cancellable dialog fragment.
*/
class ProgressCardDialogFragment : DialogFragment(R.layout.progress_card_dialog) {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
isCancelable = false
return super.onCreateDialog(savedInstanceState).apply {
this.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
}
}
}

View File

@@ -44,6 +44,33 @@ class StoryDialogLauncherFragment : DSLSettingsFragment(titleId = R.string.prefe
}
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_turn_off_stories),
onClick = {
StoryDialogs.disableStories(requireContext(), false) {
Toast.makeText(requireContext(), R.string.preferences__internal_turn_off_stories, Toast.LENGTH_SHORT).show()
}
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_turn_off_stories_with_stories_on_disk),
onClick = {
StoryDialogs.disableStories(requireContext(), true) {
Toast.makeText(requireContext(), R.string.preferences__internal_turn_off_stories_with_stories_on_disk, Toast.LENGTH_SHORT).show()
}
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_delete_private_story),
onClick = {
StoryDialogs.deleteDistributionList(requireContext(), "Family") {
Toast.makeText(requireContext(), R.string.preferences__internal_delete_private_story, Toast.LENGTH_SHORT).show()
}
}
)
}
}
}

View File

@@ -10,6 +10,38 @@ import org.thoughtcrime.securesms.R
object StoryDialogs {
fun deleteDistributionList(
context: Context,
distributionListName: String,
onDelete: () -> Unit
) {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.StoryDialogs__delete_private_story)
.setMessage(context.getString(R.string.StoryDialogs__s_and_updates_shared, distributionListName))
.setPositiveButton(R.string.StoryDialogs__delete) { _, _ -> onDelete() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
fun disableStories(
context: Context,
userHasStories: Boolean,
onDisable: () -> Unit
) {
val positiveButtonMessage = if (userHasStories) {
R.string.StoryDialogs__turn_off_and_delete
} else {
R.string.StoriesPrivacySettingsFragment__turn_off_stories
}
MaterialAlertDialogBuilder(context)
.setTitle(R.string.StoriesPrivacySettingsFragment__turn_off_stories_question)
.setMessage(R.string.StoriesPrivacySettingsFragment__you_will_no_longer_be_able_to_share)
.setPositiveButton(positiveButtonMessage) { _, _ -> onDisable() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
fun resendStory(context: Context, onDismiss: () -> Unit = {}, resend: () -> Unit) {
MaterialAlertDialogBuilder(context)
.setMessage(R.string.StoryDialogs__story_could_not_be_sent)

View File

@@ -10,6 +10,8 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.DialogFragmentDisplayManager
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
import org.thoughtcrime.securesms.components.WrapperDialogFragment
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
@@ -17,6 +19,7 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.fragments.findListener
@@ -28,6 +31,8 @@ class PrivateStorySettingsFragment : DSLSettingsFragment(
menuId = R.menu.story_private_menu
) {
private val progressDisplayManager = DialogFragmentDisplayManager { ProgressCardDialogFragment() }
private val viewModel: PrivateStorySettingsViewModel by viewModels(
factoryProducer = {
PrivateStorySettingsViewModel.Factory(PrivateStorySettingsFragmentArgs.fromBundle(requireArguments()).distributionListId, PrivateStorySettingsRepository())
@@ -49,6 +54,12 @@ class PrivateStorySettingsFragment : DSLSettingsFragment(
val toolbar: Toolbar = requireView().findViewById(R.id.toolbar)
viewModel.state.observe(viewLifecycleOwner) { state ->
if (state.isActionInProgress) {
progressDisplayManager.show(viewLifecycleOwner, childFragmentManager)
} else {
progressDisplayManager.hide()
}
toolbar.title = state.privateStory?.name
adapter.submitList(getConfiguration(state).toMappingModelList())
}
@@ -88,7 +99,8 @@ class PrivateStorySettingsFragment : DSLSettingsFragment(
clickPref(
title = DSLSettingsText.from(R.string.PrivateStorySettingsFragment__delete_private_story, DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_alert_primary))),
onClick = {
handleDeletePrivateStory()
val privateStoryName = viewModel.state.value?.privateStory?.name
handleDeletePrivateStory(privateStoryName)
}
)
}
@@ -113,13 +125,12 @@ class PrivateStorySettingsFragment : DSLSettingsFragment(
.show()
}
private fun handleDeletePrivateStory() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.PrivateStorySettingsFragment__are_you_sure)
.setMessage(R.string.PrivateStorySettingsFragment__this_action_cannot)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setPositiveButton(R.string.delete) { _, _ -> viewModel.delete().subscribe { findNavController().popBackStack() } }
.show()
private fun handleDeletePrivateStory(privateStoryName: String?) {
val name = privateStoryName ?: return
StoryDialogs.deleteDistributionList(requireContext(), name) {
viewModel.delete().subscribe { findNavController().popBackStack() }
}
}
override fun onToolbarNavigationClicked() {

View File

@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.stories.Stories
class PrivateStorySettingsRepository {
@@ -27,6 +28,13 @@ class PrivateStorySettingsRepository {
return Completable.fromAction {
SignalDatabase.distributionLists.deleteList(distributionListId)
Stories.onStorySettingsChanged(distributionListId)
val recipientId = SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionListId)
SignalDatabase.mms.getAllStoriesFor(recipientId, -1).use { reader ->
for (record in reader) {
MessageSender.sendRemoteDelete(record.id, record.isMms)
}
}
}.subscribeOn(Schedulers.io())
}

View File

@@ -4,5 +4,6 @@ import org.thoughtcrime.securesms.database.model.DistributionListRecord
data class PrivateStorySettingsState(
val privateStory: DistributionListRecord? = null,
val areRepliesAndReactionsEnabled: Boolean = false
val areRepliesAndReactionsEnabled: Boolean = false,
val isActionInProgress: Boolean = false
)

View File

@@ -52,7 +52,9 @@ class PrivateStorySettingsViewModel(private val distributionListId: Distribution
}
fun delete(): Completable {
return repository.delete(distributionListId).observeOn(AndroidSchedulers.mainThread())
return repository.delete(distributionListId)
.doOnSubscribe { store.update { it.copy(isActionInProgress = true) } }
.observeOn(AndroidSchedulers.mainThread())
}
class Factory(private val privateStoryItemData: DistributionListId, private val repository: PrivateStorySettingsRepository) : ViewModelProvider.Factory {

View File

@@ -4,9 +4,10 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ConcatAdapter
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.dp
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.DialogFragmentDisplayManager
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
@@ -17,6 +18,7 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.groups.ParcelableGroupId
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseGroupStoryBottomSheet
import org.thoughtcrime.securesms.mediasend.v2.stories.ChooseStoryTypeBottomSheet
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs
import org.thoughtcrime.securesms.stories.settings.create.CreateStoryFlowDialogFragment
import org.thoughtcrime.securesms.stories.settings.create.CreateStoryWithViewersFragment
import org.thoughtcrime.securesms.util.BottomSheetUtil
@@ -36,6 +38,7 @@ class StoriesPrivacySettingsFragment :
private val viewModel: StoriesPrivacySettingsViewModel by viewModels()
private val lifecycleDisposable = LifecycleDisposable()
private val progressDisplayManager = DialogFragmentDisplayManager { ProgressCardDialogFragment() }
override fun createAdapters(): Array<MappingAdapter> {
return arrayOf(DSLSettingsAdapter(), PagingMappingAdapter<ContactSearchKey>(), DSLSettingsAdapter())
@@ -84,6 +87,12 @@ class StoriesPrivacySettingsFragment :
}
lifecycleDisposable += viewModel.state.subscribe { state ->
if (state.isUpdatingEnabledState) {
progressDisplayManager.show(viewLifecycleOwner, childFragmentManager)
} else {
progressDisplayManager.hide()
}
(top as MappingAdapter).submitList(getTopConfiguration(state).toMappingModelList())
middle.submitList(getMiddleConfiguration(state).toMappingModelList())
(bottom as MappingAdapter).submitList(getBottomConfiguration(state).toMappingModelList())
@@ -144,12 +153,9 @@ class StoriesPrivacySettingsFragment :
DSLSettingsText.ColorModifier(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurfaceVariant))
),
onClick = {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.StoriesPrivacySettingsFragment__turn_off_stories_question)
.setMessage(R.string.StoriesPrivacySettingsFragment__you_will_no_longer_be_able_to)
.setPositiveButton(R.string.StoriesPrivacySettingsFragment__turn_off_stories) { _, _ -> viewModel.setStoriesEnabled(false) }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
StoryDialogs.disableStories(requireContext(), viewModel.userHasActiveStories) {
viewModel.setStoriesEnabled(false)
}
}
)
}

View File

@@ -1,12 +1,14 @@
package org.thoughtcrime.securesms.stories.settings.story
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.GroupDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.stories.Stories
@@ -23,6 +25,20 @@ class StoriesPrivacySettingsRepository {
return Completable.fromAction {
SignalStore.storyValues().isFeatureDisabled = !isEnabled
Stories.onStorySettingsChanged(Recipient.self().id)
SignalDatabase.mms.getAllOutgoingStories(false, -1).use { reader ->
reader.map { record -> record.id }
}.forEach { messageId ->
MessageSender.sendRemoteDelete(messageId, true)
}
}.subscribeOn(Schedulers.io())
}
fun userHasOutgoingStories(): Single<Boolean> {
return Single.fromCallable {
SignalDatabase.mms.getAllOutgoingStories(false, -1).use {
it.iterator().hasNext()
}
}.subscribeOn(Schedulers.io())
}
}

View File

@@ -5,5 +5,6 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchData
data class StoriesPrivacySettingsState(
val areStoriesEnabled: Boolean,
val isUpdatingEnabledState: Boolean = false,
val storyContactItems: List<ContactSearchData> = emptyList()
val storyContactItems: List<ContactSearchData> = emptyList(),
val userHasStories: Boolean = false
)

View File

@@ -39,6 +39,7 @@ class StoriesPrivacySettingsViewModel : ViewModel() {
private val headerActionRequestSubject = PublishSubject.create<Unit>()
val state: Flowable<StoriesPrivacySettingsState> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
val userHasActiveStories: Boolean get() = store.state.userHasStories
val pagingController = ProxyPagingController<ContactSearchKey>()
val headerActionRequests: Observable<Unit> = headerActionRequestSubject.debounce(100, TimeUnit.MILLISECONDS)
@@ -59,6 +60,8 @@ class StoriesPrivacySettingsViewModel : ViewModel() {
pagingController.set(observablePagedData.controller)
updateUserHasStories()
disposables += store.update(observablePagedData.data.toFlowable(BackpressureStrategy.LATEST)) { data, state ->
state.copy(storyContactItems = data)
}
@@ -78,6 +81,7 @@ class StoriesPrivacySettingsViewModel : ViewModel() {
areStoriesEnabled = Stories.isFeatureEnabled()
)
}
updateUserHasStories()
}
}
@@ -86,4 +90,10 @@ class StoriesPrivacySettingsViewModel : ViewModel() {
pagingController.onDataInvalidated()
}
}
private fun updateUserHasStories() {
disposables += repository.userHasOutgoingStories().subscribe { userHasActiveStories ->
store.update { it.copy(userHasStories = userHasActiveStories) }
}
}
}