mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Add dialog protection and remote deletion to disabling stories and deleting lists.
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user