From 78d4d9a3dd03733ca60c601f6569fac9d002e652 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Tue, 5 Jul 2022 11:58:02 -0400 Subject: [PATCH] Add first time My Story privacy configuration. --- .../contacts/paged/ContactSearchItems.kt | 30 ++- .../contacts/paged/ContactSearchMediator.kt | 12 +- .../forward/MultiselectForwardFragment.kt | 37 ++-- .../v2/text/TextStoryPostCreationFragment.kt | 19 +- .../v2/text/send/TextStoryPostSendFragment.kt | 27 ++- .../securesms/stories/dialogs/StoryDialogs.kt | 39 ---- .../settings/my/MyStorySettingsRepository.kt | 30 ++- .../ChangeMyStoryMembershipFragment.kt | 27 +++ ...toryMembershipBottomSheetDialogFragment.kt | 135 +++++++++++++ .../ChooseInitialMyStoryMembershipState.kt | 6 + ...ChooseInitialMyStoryMembershipViewModel.kt | 46 +++++ .../privacy/HideStoryFromDialogFragment.kt | 25 --- ...e_initial_my_story_membership_fragment.xml | 177 ++++++++++++++++++ ...ries_base_recipient_selection_fragment.xml | 2 +- app/src/main/res/values/strings.xml | 13 ++ 15 files changed, 485 insertions(+), 140 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipBottomSheetDialogFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipState.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/HideStoryFromDialogFragment.kt create mode 100644 app/src/main/res/layout/choose_initial_my_story_membership_fragment.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt index 68d8918aa..9ce06771c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchItems.kt @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.components.FromTextView import org.thoughtcrime.securesms.components.menu.ActionItem import org.thoughtcrime.securesms.components.menu.SignalContextMenu +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter @@ -55,7 +56,7 @@ object ContactSearchItems { return MappingModelList( contactSearchData.filterNotNull().map { when (it) { - is ContactSearchData.Story -> StoryModel(it, selection.contains(it.contactSearchKey)) + is ContactSearchData.Story -> StoryModel(it, selection.contains(it.contactSearchKey), SignalStore.storyValues().userHasBeenNotifiedAboutStories) is ContactSearchData.KnownRecipient -> RecipientModel(it, selection.contains(it.contactSearchKey)) is ContactSearchData.Expand -> ExpandModel(it) is ContactSearchData.Header -> HeaderModel(it) @@ -67,18 +68,23 @@ object ContactSearchItems { /** * Story Model */ - private class StoryModel(val story: ContactSearchData.Story, val isSelected: Boolean) : MappingModel { + private class StoryModel(val story: ContactSearchData.Story, val isSelected: Boolean, val hasBeenNotified: Boolean) : MappingModel { override fun areItemsTheSame(newItem: StoryModel): Boolean { return newItem.story == story } override fun areContentsTheSame(newItem: StoryModel): Boolean { - return story.recipient.hasSameContent(newItem.story.recipient) && isSelected == newItem.isSelected + return story.recipient.hasSameContent(newItem.story.recipient) && + isSelected == newItem.isSelected && + hasBeenNotified == newItem.hasBeenNotified } override fun getChangePayload(newItem: StoryModel): Any? { - return if (story.recipient.hasSameContent(newItem.story.recipient) && newItem.isSelected != isSelected) { + return if (story.recipient.hasSameContent(newItem.story.recipient) && + hasBeenNotified == newItem.hasBeenNotified && + newItem.isSelected != isSelected + ) { 0 } else { null @@ -100,13 +106,17 @@ object ContactSearchItems { model.story.viewerCount } - val pluralId = when { - model.story.recipient.isGroup -> R.plurals.ContactSearchItems__group_story_d_viewers - model.story.recipient.isMyStory -> R.plurals.SelectViewersFragment__d_viewers - else -> R.plurals.ContactSearchItems__private_story_d_viewers - } + if (model.story.recipient.isMyStory && !model.hasBeenNotified) { + number.setText(R.string.ContactSearchItems__tap_to_choose_your_viewers) + } else { + val pluralId = when { + model.story.recipient.isGroup -> R.plurals.ContactSearchItems__group_story_d_viewers + model.story.recipient.isMyStory -> R.plurals.SelectViewersFragment__d_viewers + else -> R.plurals.ContactSearchItems__private_story_d_viewers + } - number.text = context.resources.getQuantityString(pluralId, count, count) + number.text = context.resources.getQuantityString(pluralId, count, count) + } } override fun bindLongPress(model: StoryModel) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt index cfa179072..4cb29c6b8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt @@ -9,8 +9,10 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.groups.SelectionLimits +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.stories.settings.custom.PrivateStorySettingsFragment import org.thoughtcrime.securesms.stories.settings.my.MyStorySettingsFragment +import org.thoughtcrime.securesms.stories.settings.privacy.ChooseInitialMyStoryMembershipBottomSheetDialogFragment import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter import org.thoughtcrime.securesms.util.livedata.LiveDataUtil @@ -35,7 +37,7 @@ class ContactSearchMediator( mappingAdapter = adapter, displayCheckBox = displayCheckBox, recipientListener = this::toggleSelection, - storyListener = this::toggleSelection, + storyListener = this::toggleStorySelection, storyContextMenuCallbacks = StoryContextMenuCallbacks(), expandListener = { viewModel.expandSection(it.sectionKey) } ) @@ -87,6 +89,14 @@ class ContactSearchMediator( viewModel.refresh() } + private fun toggleStorySelection(view: View, contactSearchData: ContactSearchData.Story, isSelected: Boolean) { + if (contactSearchData.recipient.isMyStory && !SignalStore.storyValues().userHasBeenNotifiedAboutStories) { + ChooseInitialMyStoryMembershipBottomSheetDialogFragment.show(fragment.childFragmentManager) + } else { + toggleSelection(view, contactSearchData, isSelected) + } + } + private fun toggleSelection(view: View, contactSearchData: ContactSearchData, isSelected: Boolean) { return if (isSelected) { viewModel.setKeysNotSelected(setOf(contactSearchData.contactSearchKey)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt index db15f6fa7..8e51c0c82 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/mutiselect/forward/MultiselectForwardFragment.kt @@ -45,10 +45,9 @@ import org.thoughtcrime.securesms.sharing.ShareSelectionAdapter import org.thoughtcrime.securesms.sharing.ShareSelectionMappingModel import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.stories.Stories.getHeaderAction -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.stories.settings.privacy.HideStoryFromDialogFragment +import org.thoughtcrime.securesms.stories.settings.privacy.ChooseInitialMyStoryMembershipBottomSheetDialogFragment import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.FullscreenHelper @@ -77,7 +76,8 @@ class MultiselectForwardFragment : Fragment(R.layout.multiselect_forward_fragment), SafetyNumberChangeDialog.Callback, ChooseStoryTypeBottomSheet.Callback, - WrapperDialogFragment.WrapperDialogFragmentCallback { + WrapperDialogFragment.WrapperDialogFragmentCallback, + ChooseInitialMyStoryMembershipBottomSheetDialogFragment.Callback { private val viewModel: MultiselectForwardViewModel by viewModels(factoryProducer = this::createViewModelFactory) private val disposables = LifecycleDisposable() @@ -285,24 +285,6 @@ class MultiselectForwardFragment : private fun onSend(sendButton: View) { sendButton.isEnabled = false - - StoryDialogs.guardWithAddToYourStoryDialog( - requireContext(), - contactSearchMediator.getSelectedContacts(), - onAddToStory = { - performSend() - }, - onEditViewers = { - sendButton.isEnabled = true - HideStoryFromDialogFragment().show(childFragmentManager, null) - }, - onCancel = { - sendButton.isEnabled = true - } - ) - } - - private fun performSend() { viewModel.send(addMessage.text.toString(), contactSearchMediator.getSelectedContacts()) } @@ -483,6 +465,15 @@ class MultiselectForwardFragment : CreateStoryFlowDialogFragment().show(parentFragmentManager, CreateStoryWithViewersFragment.REQUEST_KEY) } + override fun onWrapperDialogFragmentDismissed() { + contactSearchMediator.refresh() + } + + override fun onMyStoryConfigured(recipientId: RecipientId) { + contactSearchMediator.setKeysSelected(setOf(ContactSearchKey.RecipientSearchKey.Story(recipientId))) + contactSearchMediator.refresh() + } + interface Callback { fun onFinishForwardAction() fun exitFlow() @@ -544,8 +535,4 @@ class MultiselectForwardFragment : } } } - - override fun onWrapperDialogFragmentDismissed() { - contactSearchMediator.refresh() - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt index 3d989ff3a..588e111c5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/TextStoryPostCreationFragment.kt @@ -22,8 +22,6 @@ import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel import org.thoughtcrime.securesms.mediasend.v2.text.send.TextStoryPostSendRepository import org.thoughtcrime.securesms.mediasend.v2.text.send.TextStoryPostSendResult import org.thoughtcrime.securesms.stories.StoryTextPostView -import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs -import org.thoughtcrime.securesms.stories.settings.privacy.HideStoryFromDialogFragment import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.navigation.safeNavigate @@ -134,22 +132,7 @@ class TextStoryPostCreationFragment : Fragment(R.layout.stories_text_post_creati findNavController().safeNavigate(R.id.action_textStoryPostCreationFragment_to_textStoryPostSendFragment) } else { send.isClickable = false - StoryDialogs.guardWithAddToYourStoryDialog( - contacts = contacts, - context = requireContext(), - onAddToStory = { - performSend(contacts) - }, - onEditViewers = { - send.isClickable = true - storyTextPostView.hideCloseButton() - HideStoryFromDialogFragment().show(childFragmentManager, null) - }, - onCancel = { - send.isClickable = true - storyTextPostView.hideCloseButton() - } - ) + performSend(contacts) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendFragment.kt index a29e9af18..fad87ec51 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/text/send/TextStoryPostSendFragment.kt @@ -27,16 +27,19 @@ import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.sharing.ShareSelectionAdapter import org.thoughtcrime.securesms.sharing.ShareSelectionMappingModel import org.thoughtcrime.securesms.stories.Stories -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.stories.settings.privacy.HideStoryFromDialogFragment +import org.thoughtcrime.securesms.stories.settings.privacy.ChooseInitialMyStoryMembershipBottomSheetDialogFragment import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.livedata.LiveDataUtil -class TextStoryPostSendFragment : Fragment(R.layout.stories_send_text_post_fragment), ChooseStoryTypeBottomSheet.Callback, WrapperDialogFragment.WrapperDialogFragmentCallback { +class TextStoryPostSendFragment : + Fragment(R.layout.stories_send_text_post_fragment), + ChooseStoryTypeBottomSheet.Callback, + WrapperDialogFragment.WrapperDialogFragmentCallback, + ChooseInitialMyStoryMembershipBottomSheetDialogFragment.Callback { private lateinit var shareListWrapper: View private lateinit var shareSelectionRecyclerView: RecyclerView @@ -87,18 +90,7 @@ class TextStoryPostSendFragment : Fragment(R.layout.stories_send_text_post_fragm shareConfirmButton.setOnClickListener { viewModel.onSending() - StoryDialogs.guardWithAddToYourStoryDialog( - contacts = contactSearchMediator.getSelectedContacts(), - context = requireContext(), - onAddToStory = { send() }, - onEditViewers = { - viewModel.onSendCancelled() - HideStoryFromDialogFragment().show(childFragmentManager, null) - }, - onCancel = { - viewModel.onSendCancelled() - } - ) + send() } disposables += viewModel.untrustedIdentities.subscribe { @@ -200,4 +192,9 @@ class TextStoryPostSendFragment : Fragment(R.layout.stories_send_text_post_fragm override fun onWrapperDialogFragmentDismissed() { contactSearchMediator.refresh() } + + override fun onMyStoryConfigured(recipientId: RecipientId) { + contactSearchMediator.setKeysSelected(setOf(ContactSearchKey.RecipientSearchKey.Story(recipientId))) + contactSearchMediator.refresh() + } } 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 8416ad1f2..f4d5c2e21 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 @@ -2,53 +2,14 @@ package org.thoughtcrime.securesms.stories.dialogs import android.content.Context import android.view.View -import android.widget.Toast import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import org.signal.core.util.DimensionUnit import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey -import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.recipients.Recipient object StoryDialogs { - /** - * Guards onAddToStory with a dialog - */ - fun guardWithAddToYourStoryDialog( - context: Context, - contacts: Collection, - onAddToStory: () -> Unit, - onEditViewers: () -> Unit, - onCancel: () -> Unit = {} - ) { - if (!isFirstSendToMyStory(contacts)) { - onAddToStory() - } else { - SignalStore.storyValues().userHasBeenNotifiedAboutStories = true - MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Signal_MaterialAlertDialog) - .setTitle(R.string.StoryDialogs__add_to_story_q) - .setMessage(R.string.StoryDialogs__adding_content) - .setPositiveButton(R.string.StoryDialogs__add_to_story) { _, _ -> - onAddToStory.invoke() - } - .setNeutralButton(R.string.StoryDialogs__edit_viewers) { _, _ -> Toast.makeText(context, "New flow coming soon", Toast.LENGTH_SHORT).show() } - .setNegativeButton(android.R.string.cancel) { _, _ -> onCancel.invoke() } - .setCancelable(false) - .show() - } - } - - private fun isFirstSendToMyStory(shareContacts: Collection): Boolean { - if (SignalStore.storyValues().userHasBeenNotifiedAboutStories) { - return false - } - - return shareContacts.any { it is ContactSearchKey.RecipientSearchKey.Story && Recipient.resolved(it.recipientId).isMyStory } - } - fun resendStory(context: Context, 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/my/MyStorySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsRepository.kt index a0a55c279..9cc63d3f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/my/MyStorySettingsRepository.kt @@ -1,27 +1,35 @@ package org.thoughtcrime.securesms.stories.settings.my +import androidx.annotation.WorkerThread import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.DistributionListPrivacyData import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode +import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.stories.Stories +import org.thoughtcrime.securesms.stories.settings.privacy.ChooseInitialMyStoryMembershipState class MyStorySettingsRepository { fun getPrivacyState(): Single { return Single.fromCallable { - val privacyData: DistributionListPrivacyData = SignalDatabase.distributionLists.getPrivacyData(DistributionListId.MY_STORY) - - MyStoryPrivacyState( - privacyMode = privacyData.privacyMode, - connectionCount = if (privacyData.privacyMode == DistributionListPrivacyMode.ALL_EXCEPT) privacyData.rawMemberCount else privacyData.memberCount - ) + getStoryPrivacyState() }.subscribeOn(Schedulers.io()) } + fun observeChooseInitialPrivacy(): Observable { + return Single.fromCallable { SignalDatabase.distributionLists.getRecipientId(DistributionListId.MY_STORY)!! } + .subscribeOn(Schedulers.io()) + .flatMapObservable { recipientId -> + Recipient.observable(recipientId) + .flatMap { Observable.just(ChooseInitialMyStoryMembershipState(recipientId = recipientId, privacyState = getStoryPrivacyState())) } + } + } + fun setPrivacyMode(privacyMode: DistributionListPrivacyMode): Completable { return Completable.fromAction { SignalDatabase.distributionLists.setPrivacyMode(DistributionListId.MY_STORY, privacyMode) @@ -41,4 +49,14 @@ class MyStorySettingsRepository { Stories.onStorySettingsChanged(DistributionListId.MY_STORY) }.subscribeOn(Schedulers.io()) } + + @WorkerThread + private fun getStoryPrivacyState(): MyStoryPrivacyState { + val privacyData: DistributionListPrivacyData = SignalDatabase.distributionLists.getPrivacyData(DistributionListId.MY_STORY) + + return MyStoryPrivacyState( + privacyMode = privacyData.privacyMode, + connectionCount = if (privacyData.privacyMode == DistributionListPrivacyMode.ALL_EXCEPT) privacyData.rawMemberCount else privacyData.memberCount + ) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChangeMyStoryMembershipFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChangeMyStoryMembershipFragment.kt index 40ad0b79b..db1b3833e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChangeMyStoryMembershipFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChangeMyStoryMembershipFragment.kt @@ -1,7 +1,10 @@ package org.thoughtcrime.securesms.stories.settings.privacy import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.WrapperDialogFragment import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.stories.settings.select.BaseStoryRecipientSelectionFragment @@ -20,6 +23,18 @@ abstract class ChangeMyStoryMembershipFragment : BaseStoryRecipientSelectionFrag class AllExceptFragment : ChangeMyStoryMembershipFragment() { override val toolbarTitleId: Int = R.string.ChangeMyStoryMembershipFragment__all_except override val checkboxResource: Int = R.drawable.contact_selection_exclude_checkbox + + class Dialog : WrapperDialogFragment() { + override fun getWrappedFragment(): Fragment { + return AllExceptFragment() + } + } + + companion object { + fun createAsDialog(): DialogFragment { + return Dialog() + } + } } /** @@ -27,4 +42,16 @@ class AllExceptFragment : ChangeMyStoryMembershipFragment() { */ class OnlyShareWithFragment : ChangeMyStoryMembershipFragment() { override val toolbarTitleId: Int = R.string.ChangeMyStoryMembershipFragment__only_share_with + + class Dialog : WrapperDialogFragment() { + override fun getWrappedFragment(): Fragment { + return OnlyShareWithFragment() + } + } + + companion object { + fun createAsDialog(): DialogFragment { + return Dialog() + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipBottomSheetDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipBottomSheetDialogFragment.kt new file mode 100644 index 000000000..a29cc568e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipBottomSheetDialogFragment.kt @@ -0,0 +1,135 @@ +package org.thoughtcrime.securesms.stories.settings.privacy + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.viewModels +import com.google.android.material.radiobutton.MaterialRadioButton +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment +import org.thoughtcrime.securesms.components.WrapperDialogFragment +import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.stories.settings.select.BaseStoryRecipientSelectionFragment +import org.thoughtcrime.securesms.util.BottomSheetUtil +import org.thoughtcrime.securesms.util.LifecycleDisposable +import org.thoughtcrime.securesms.util.fragments.findListener +import org.thoughtcrime.securesms.util.visible + +/** + * Choose the initial settings for My Story when first sending to My Story. + */ +class ChooseInitialMyStoryMembershipBottomSheetDialogFragment : + FixedRoundedCornerBottomSheetDialogFragment(), + WrapperDialogFragment.WrapperDialogFragmentCallback, + BaseStoryRecipientSelectionFragment.Callback { + + private val viewModel: ChooseInitialMyStoryMembershipViewModel by viewModels() + + private lateinit var lifecycleDisposable: LifecycleDisposable + + private lateinit var allRow: View + private lateinit var allExceptRow: View + private lateinit var onlyWitRow: View + + private lateinit var allRadio: MaterialRadioButton + private lateinit var allExceptRadio: MaterialRadioButton + private lateinit var onlyWitRadio: MaterialRadioButton + + private lateinit var allExceptCount: TextView + private lateinit var onlyWithCount: TextView + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.choose_initial_my_story_membership_fragment, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + allRow = view.findViewById(R.id.choose_initial_my_story_all_signal_connnections_row) + allExceptRow = view.findViewById(R.id.choose_initial_my_story_all_signal_connnections_except_row) + onlyWitRow = view.findViewById(R.id.choose_initial_my_story_only_share_with_row) + + allRadio = view.findViewById(R.id.choose_initial_my_story_all_signal_connnections_radio) + allExceptRadio = view.findViewById(R.id.choose_initial_my_story_all_signal_connnections_except_radio) + onlyWitRadio = view.findViewById(R.id.choose_initial_my_story_only_share_with_radio) + + allExceptCount = view.findViewById(R.id.choose_initial_my_story_all_signal_connnections_except_count) + onlyWithCount = view.findViewById(R.id.choose_initial_my_story_only_share_with_count) + + val save = view.findViewById(R.id.choose_initial_my_story_save).apply { + isEnabled = false + } + + lifecycleDisposable = LifecycleDisposable().apply { bindTo(viewLifecycleOwner) } + + lifecycleDisposable += viewModel.state + .subscribe { state -> + allRadio.isChecked = state.privacyState.privacyMode == DistributionListPrivacyMode.ALL + allExceptRadio.isChecked = state.privacyState.privacyMode == DistributionListPrivacyMode.ALL_EXCEPT + onlyWitRadio.isChecked = state.privacyState.privacyMode == DistributionListPrivacyMode.ONLY_WITH + + allExceptCount.visible = allExceptRadio.isChecked + onlyWithCount.visible = onlyWitRadio.isChecked + + when (state.privacyState.privacyMode) { + DistributionListPrivacyMode.ALL_EXCEPT -> allExceptCount.text = resources.getQuantityString(R.plurals.MyStorySettingsFragment__d_people_excluded, state.privacyState.connectionCount, state.privacyState.connectionCount) + DistributionListPrivacyMode.ONLY_WITH -> onlyWithCount.text = resources.getQuantityString(R.plurals.MyStorySettingsFragment__d_people, state.privacyState.connectionCount, state.privacyState.connectionCount) + else -> Unit + } + + save.isEnabled = state.recipientId != null + } + + val clickListener = { v: View -> + val selection = when (v) { + allRow -> DistributionListPrivacyMode.ALL + allExceptRow -> DistributionListPrivacyMode.ALL_EXCEPT + onlyWitRow -> DistributionListPrivacyMode.ONLY_WITH + else -> throw AssertionError() + } + + viewModel + .select(selection) + .subscribe { confirmedSelection -> + when (confirmedSelection) { + DistributionListPrivacyMode.ALL_EXCEPT -> AllExceptFragment.createAsDialog().show(childFragmentManager, SELECTION_FRAGMENT) + DistributionListPrivacyMode.ONLY_WITH -> OnlyShareWithFragment.createAsDialog().show(childFragmentManager, SELECTION_FRAGMENT) + else -> Unit + } + } + } + + listOf(allRow, allExceptRow, onlyWitRow).forEach { it.setOnClickListener { v -> clickListener(v) } } + + save.setOnClickListener { + lifecycleDisposable += viewModel + .save() + .subscribe { recipientId -> + dismissAllowingStateLoss() + findListener()?.onMyStoryConfigured(recipientId) + } + } + } + + override fun exitFlow() { + (childFragmentManager.findFragmentByTag(SELECTION_FRAGMENT) as? DialogFragment)?.dismissAllowingStateLoss() + } + + override fun onWrapperDialogFragmentDismissed() = Unit + + companion object { + private const val SELECTION_FRAGMENT = "selection_fragment" + + fun show(fragmentManager: FragmentManager) { + val fragment = ChooseInitialMyStoryMembershipBottomSheetDialogFragment() + fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + } + } + + interface Callback { + fun onMyStoryConfigured(recipientId: RecipientId) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipState.kt new file mode 100644 index 000000000..d284a8a10 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipState.kt @@ -0,0 +1,6 @@ +package org.thoughtcrime.securesms.stories.settings.privacy + +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.stories.settings.my.MyStoryPrivacyState + +data class ChooseInitialMyStoryMembershipState(val recipientId: RecipientId? = null, val privacyState: MyStoryPrivacyState = MyStoryPrivacyState()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt new file mode 100644 index 000000000..5b06efa8b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/ChooseInitialMyStoryMembershipViewModel.kt @@ -0,0 +1,46 @@ +package org.thoughtcrime.securesms.stories.settings.privacy + +import androidx.lifecycle.ViewModel +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Flowable +import io.reactivex.rxjava3.core.Single +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.kotlin.plusAssign +import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.stories.settings.my.MyStorySettingsRepository +import org.thoughtcrime.securesms.util.rx.RxStore + +class ChooseInitialMyStoryMembershipViewModel @JvmOverloads constructor( + private val repository: MyStorySettingsRepository = MyStorySettingsRepository() +) : ViewModel() { + + private val store = RxStore(ChooseInitialMyStoryMembershipState()) + private val disposables = CompositeDisposable() + + val state: Flowable = store.stateFlowable.observeOn(AndroidSchedulers.mainThread()) + + init { + disposables += repository.observeChooseInitialPrivacy() + .distinctUntilChanged() + .subscribe { state -> store.update { state } } + } + + override fun onCleared() { + disposables.clear() + } + + fun select(selection: DistributionListPrivacyMode): Single { + return repository.setPrivacyMode(selection) + .toSingleDefault(selection) + .observeOn(AndroidSchedulers.mainThread()) + } + + fun save(): Single { + return Single.fromCallable { + SignalStore.storyValues().userHasBeenNotifiedAboutStories = true + store.state.recipientId + }.observeOn(AndroidSchedulers.mainThread()) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/HideStoryFromDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/HideStoryFromDialogFragment.kt deleted file mode 100644 index 13e9476a1..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/privacy/HideStoryFromDialogFragment.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.thoughtcrime.securesms.stories.settings.privacy - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.DialogFragment -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.stories.settings.select.BaseStoryRecipientSelectionFragment - -/** - * Embeds HideStoryFromFragment in a full-screen dialog. - */ -class HideStoryFromDialogFragment : DialogFragment(R.layout.fragment_container), BaseStoryRecipientSelectionFragment.Callback { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - // TODO [stories] replace with new bottom sheet - } - - override fun exitFlow() { - dismissAllowingStateLoss() - } -} diff --git a/app/src/main/res/layout/choose_initial_my_story_membership_fragment.xml b/app/src/main/res/layout/choose_initial_my_story_membership_fragment.xml new file mode 100644 index 000000000..8f7cdf7ee --- /dev/null +++ b/app/src/main/res/layout/choose_initial_my_story_membership_fragment.xml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/stories_base_recipient_selection_fragment.xml b/app/src/main/res/layout/stories_base_recipient_selection_fragment.xml index 8c78abd9d..ab96b3840 100644 --- a/app/src/main/res/layout/stories_base_recipient_selection_fragment.xml +++ b/app/src/main/res/layout/stories_base_recipient_selection_fragment.xml @@ -39,7 +39,7 @@ Group Story · %1$d viewer Group Story · %1$d viewers + + Tap to choose your viewers Story settings @@ -4933,6 +4935,17 @@ Got it + + My Story Privacy + + Choose who can see posts to My Story. You can always make changes in settings. + + All Signal connections + + All Signal connections except… + + Only share with… +