diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DialogFragmentDisplayManager.kt b/app/src/main/java/org/thoughtcrime/securesms/components/DialogFragmentDisplayManager.kt new file mode 100644 index 000000000..c6b16998e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/DialogFragmentDisplayManager.kt @@ -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() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ProgressCardDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/ProgressCardDialogFragment.kt new file mode 100644 index 000000000..41a649a81 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ProgressCardDialogFragment.kt @@ -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)) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/StoryDialogLauncherFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/StoryDialogLauncherFragment.kt index b8946dc4f..bc5dff209 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/StoryDialogLauncherFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/StoryDialogLauncherFragment.kt @@ -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() + } + } + ) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt index db1c781a5..37fc18de3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/dialogs/StoryDialogs.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsFragment.kt index 89ae68b78..c4aea452b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsFragment.kt @@ -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() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsRepository.kt index 9a1c5037e..3d6a9835c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsRepository.kt @@ -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()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsState.kt index b89926b7a..44d727242 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsState.kt @@ -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 ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsViewModel.kt index c4197bf99..93e4acbbd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/custom/PrivateStorySettingsViewModel.kt @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt index 3070657d0..388793e2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt @@ -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 { return arrayOf(DSLSettingsAdapter(), PagingMappingAdapter(), 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) + } } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt index ca97f0616..7e859cd6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt @@ -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 { + return Single.fromCallable { + SignalDatabase.mms.getAllOutgoingStories(false, -1).use { + it.iterator().hasNext() + } }.subscribeOn(Schedulers.io()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsState.kt index bc1e0b3b4..74107df97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsState.kt @@ -5,5 +5,6 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchData data class StoriesPrivacySettingsState( val areStoriesEnabled: Boolean, val isUpdatingEnabledState: Boolean = false, - val storyContactItems: List = emptyList() + val storyContactItems: List = emptyList(), + val userHasStories: Boolean = false ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt index f32601584..be46426b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt @@ -39,6 +39,7 @@ class StoriesPrivacySettingsViewModel : ViewModel() { private val headerActionRequestSubject = PublishSubject.create() val state: Flowable = store.stateFlowable.observeOn(AndroidSchedulers.mainThread()) + val userHasActiveStories: Boolean get() = store.state.userHasStories val pagingController = ProxyPagingController() val headerActionRequests: Observable = 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) } + } + } } diff --git a/app/src/main/res/layout/progress_card.xml b/app/src/main/res/layout/progress_card.xml index 0bd315bb5..413df3950 100644 --- a/app/src/main/res/layout/progress_card.xml +++ b/app/src/main/res/layout/progress_card.xml @@ -12,5 +12,6 @@ android:layout_gravity="center" android:layout_margin="24dp" android:indeterminate="true" + android:background="@color/transparent" app:indicatorColor="@color/signal_colorPrimary" /> diff --git a/app/src/main/res/layout/progress_card_dialog.xml b/app/src/main/res/layout/progress_card_dialog.xml new file mode 100644 index 000000000..2130169a6 --- /dev/null +++ b/app/src/main/res/layout/progress_card_dialog.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d2038bcc6..32443b6da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2753,6 +2753,9 @@ Customize option + Turn off stories (with stories on disk) + Turn off stories + Delete private story Hide story Story or profile selector Stories dialog launcher @@ -4936,6 +4939,12 @@ Only share with… Done + + Delete private story? + + \"%1$s\" and updates shared to this story will be deleted. + + Delete Add to story? @@ -4948,6 +4957,8 @@ Story could not be sent. Check your connection and try again. Send + + Turn off and delete Share & View Stories @@ -5231,7 +5242,7 @@ Turn off stories? - You will no longer be able to share or view stories. Any stories you have recently sent will still be visible by others until they expire. + You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted. Story privacy