From d45acd0e24b5d5192b017203f65a87d8508ede1e Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Wed, 7 Aug 2024 16:39:29 -0400 Subject: [PATCH] Migrate existing Reminders to Banners. --- .../thoughtcrime/securesms/banner/Banner.kt | 2 +- .../banner/banners/BubbleOptOutBanner.kt | 44 ++++++ .../banner/banners/CdsPermanentErrorBanner.kt | 53 +++++++ .../banner/banners/CdsTemporaryErrorBanner.kt | 46 ++++++ .../securesms/banner/banners/DozeBanner.kt | 55 +++++++ ...BuildBanner.kt => EnclaveFailureBanner.kt} | 29 ++-- .../GroupsV1MigrationSuggestionsBanner.kt | 45 ++++++ .../banner/banners/OutdatedBuildBanner.kt | 63 ++++++++ .../banners/PendingGroupJoinRequestsBanner.kt | 49 +++++++ .../banner/banners/ServiceOutageBanner.kt | 38 +++++ .../banner/banners/UnauthorizedBanner.kt | 47 ++++++ .../banner/banners/UsernameOutOfSyncBanner.kt | 54 +++++++ .../banner/ui/compose/DefaultBanner.kt | 134 ++++++++---------- .../reminder/ExpiredBuildReminder.java | 5 - .../ConversationListFragment.java | 4 +- .../preferences/PaymentsHomeFragment.java | 53 ++++--- .../res/layout/payments_home_fragment.xml | 9 ++ 17 files changed, 608 insertions(+), 122 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/banner/banners/BubbleOptOutBanner.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsPermanentErrorBanner.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsTemporaryErrorBanner.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/banner/banners/DozeBanner.kt rename app/src/main/java/org/thoughtcrime/securesms/banner/banners/{ExpiredBuildBanner.kt => EnclaveFailureBanner.kt} (52%) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/banner/banners/GroupsV1MigrationSuggestionsBanner.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/banner/banners/OutdatedBuildBanner.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/banner/banners/PendingGroupJoinRequestsBanner.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/banner/banners/ServiceOutageBanner.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/banner/banners/UnauthorizedBanner.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/banner/banners/UsernameOutOfSyncBanner.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/Banner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/Banner.kt index 089a63d6a5..aa658db26e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/Banner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/Banner.kt @@ -37,7 +37,7 @@ abstract class Banner { /** * Whether or not the [Banner] should be shown (enabled) or hidden (disabled). */ - abstract var enabled: Boolean + abstract val enabled: Boolean /** * Composable function to display content when [enabled] is true. diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/BubbleOptOutBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/BubbleOptOutBanner.kt new file mode 100644 index 0000000000..8ae1b70012 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/BubbleOptOutBanner.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.banner.banners + +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import kotlinx.coroutines.flow.Flow +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.ui.compose.Action +import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner +import org.thoughtcrime.securesms.keyvalue.SignalStore + +class BubbleOptOutBanner(inBubble: Boolean, private val actionListener: (Boolean) -> Unit) : Banner() { + + override val enabled: Boolean = inBubble && !SignalStore.tooltips.hasSeenBubbleOptOutTooltip() && Build.VERSION.SDK_INT > 29 + + @Composable + override fun DisplayBanner() { + DefaultBanner( + title = null, + body = stringResource(id = R.string.BubbleOptOutTooltip__description), + actions = listOf( + Action(R.string.BubbleOptOutTooltip__turn_off) { + actionListener(true) + }, + Action(R.string.BubbleOptOutTooltip__not_now) { + actionListener(false) + } + ) + ) + } + + companion object { + @JvmStatic + fun createFlow(inBubble: Boolean, actionListener: (Boolean) -> Unit): Flow = createAndEmit { + BubbleOptOutBanner(inBubble, actionListener) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsPermanentErrorBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsPermanentErrorBanner.kt new file mode 100644 index 0000000000..3b119cd169 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsPermanentErrorBanner.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.banner.banners + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.fragment.app.FragmentManager +import kotlinx.coroutines.flow.Flow +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.ui.compose.Action +import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner +import org.thoughtcrime.securesms.banner.ui.compose.Importance +import org.thoughtcrime.securesms.contacts.sync.CdsPermanentErrorBottomSheet +import org.thoughtcrime.securesms.keyvalue.SignalStore +import kotlin.time.Duration.Companion.days + +class CdsPermanentErrorBanner(private val fragmentManager: FragmentManager) : Banner() { + private val timeUntilUnblock = SignalStore.misc.cdsBlockedUtil - System.currentTimeMillis() + + override val enabled: Boolean = SignalStore.misc.isCdsBlocked && timeUntilUnblock >= PERMANENT_TIME_CUTOFF + + @Composable + override fun DisplayBanner() { + DefaultBanner( + title = null, + body = stringResource(id = R.string.reminder_cds_permanent_error_body), + importance = Importance.ERROR, + actions = listOf( + Action(R.string.reminder_cds_permanent_error_learn_more) { + CdsPermanentErrorBottomSheet.show(fragmentManager) + } + ) + ) + } + + companion object { + + /** + * Even if we're not truly "permanently blocked", if the time until we're unblocked is long enough, we'd rather show the permanent error message than + * telling the user to wait for 3 months or something. + */ + val PERMANENT_TIME_CUTOFF = 30.days.inWholeMilliseconds + + @JvmStatic + fun createFlow(fragmentManager: FragmentManager): Flow = createAndEmit { + CdsPermanentErrorBanner(fragmentManager) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsTemporaryErrorBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsTemporaryErrorBanner.kt new file mode 100644 index 0000000000..e1c4db1ee0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsTemporaryErrorBanner.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.banner.banners + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.fragment.app.FragmentManager +import kotlinx.coroutines.flow.Flow +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.ui.compose.Action +import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner +import org.thoughtcrime.securesms.banner.ui.compose.Importance +import org.thoughtcrime.securesms.contacts.sync.CdsTemporaryErrorBottomSheet +import org.thoughtcrime.securesms.keyvalue.SignalStore + +class CdsTemporaryErrorBanner(private val fragmentManager: FragmentManager) : Banner() { + private val timeUntilUnblock = SignalStore.misc.cdsBlockedUtil - System.currentTimeMillis() + + override val enabled: Boolean = SignalStore.misc.isCdsBlocked && timeUntilUnblock < CdsPermanentErrorBanner.PERMANENT_TIME_CUTOFF + + @Composable + override fun DisplayBanner() { + DefaultBanner( + title = null, + body = stringResource(id = R.string.reminder_cds_warning_body), + importance = Importance.ERROR, + actions = listOf( + Action(R.string.reminder_cds_warning_learn_more) { + CdsTemporaryErrorBottomSheet.show(fragmentManager) + } + ) + ) + } + + companion object { + + @JvmStatic + fun createFlow(fragmentManager: FragmentManager): Flow = createAndEmit { + CdsTemporaryErrorBanner(fragmentManager) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/DozeBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/DozeBanner.kt new file mode 100644 index 0000000000..41721b544e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/DozeBanner.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.banner.banners + +import android.content.Context +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import kotlinx.coroutines.flow.Flow +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.ui.compose.Action +import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.util.PowerManagerCompat +import org.thoughtcrime.securesms.util.ServiceUtil +import org.thoughtcrime.securesms.util.TextSecurePreferences + +@RequiresApi(23) +class DozeBanner(private val context: Context) : Banner() { + override val enabled: Boolean = !SignalStore.account.fcmEnabled && !TextSecurePreferences.hasPromptedOptimizeDoze(context) && Build.VERSION.SDK_INT >= 23 && !ServiceUtil.getPowerManager(context).isIgnoringBatteryOptimizations(context.packageName) + + @Composable + override fun DisplayBanner() { + DefaultBanner( + title = stringResource(id = R.string.DozeReminder_optimize_for_missing_play_services), + body = stringResource(id = R.string.DozeReminder_this_device_does_not_support_play_services_tap_to_disable_system_battery), + actions = listOf( + Action(android.R.string.ok) { + TextSecurePreferences.setPromptedOptimizeDoze(context, true) + PowerManagerCompat.requestIgnoreBatteryOptimizations(context) + } + ), + onDismissListener = { + TextSecurePreferences.setPromptedOptimizeDoze(context, true) + } + ) + } + + companion object { + + @JvmStatic + fun createFlow(context: Context): Flow = createAndEmit { + if (Build.VERSION.SDK_INT >= 23) { + DozeBanner(context) + } else { + null + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/ExpiredBuildBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/EnclaveFailureBanner.kt similarity index 52% rename from app/src/main/java/org/thoughtcrime/securesms/banner/banners/ExpiredBuildBanner.kt rename to app/src/main/java/org/thoughtcrime/securesms/banner/banners/EnclaveFailureBanner.kt index e1990be11c..e6f5622241 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/ExpiredBuildBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/EnclaveFailureBanner.kt @@ -9,48 +9,35 @@ import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.banner.Banner import org.thoughtcrime.securesms.banner.ui.compose.Action import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner import org.thoughtcrime.securesms.banner.ui.compose.Importance -import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.util.PlayStoreUtil -/** - * Banner to let the user know their build is about to expire. - * - * This serves as an example for how we can replicate the functionality of the old [org.thoughtcrime.securesms.components.reminder.Reminder] system purely in the new [Banner] system. - */ -class ExpiredBuildBanner(val context: Context) : Banner() { - - override var enabled = true +class EnclaveFailureBanner(enclaveFailed: Boolean, private val context: Context) : Banner() { + override val enabled: Boolean = enclaveFailed @Composable override fun DisplayBanner() { DefaultBanner( title = null, - body = stringResource(id = R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today), - importance = Importance.TERMINAL, - isDismissible = false, + body = stringResource(id = R.string.EnclaveFailureReminder_update_signal), + importance = Importance.ERROR, actions = listOf( Action(R.string.ExpiredBuildReminder_update_now) { PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context) } - ), - onHideListener = {}, - onDismissListener = {} + ) ) } companion object { @JvmStatic - fun createFlow(context: Context): Flow = createAndEmit { - if (SignalStore.misc.isClientDeprecated) { - ExpiredBuildBanner(context) - } else { - null - } + fun Flow.mapBooleanFlowToBannerFlow(context: Context): Flow { + return map { EnclaveFailureBanner(it, context) } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/GroupsV1MigrationSuggestionsBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/GroupsV1MigrationSuggestionsBanner.kt new file mode 100644 index 0000000000..06a9b7aba4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/GroupsV1MigrationSuggestionsBanner.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.banner.banners + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.pluralStringResource +import kotlinx.coroutines.flow.Flow +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.ui.compose.Action +import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner +import org.thoughtcrime.securesms.keyvalue.SignalStore + +class GroupsV1MigrationSuggestionsBanner(private val suggestionsSize: Int, private val onAddMembers: () -> Unit, private val onNoThanks: () -> Unit) : Banner() { + private val timeUntilUnblock = SignalStore.misc.cdsBlockedUtil - System.currentTimeMillis() + + override val enabled: Boolean = SignalStore.misc.isCdsBlocked && timeUntilUnblock < CdsPermanentErrorBanner.PERMANENT_TIME_CUTOFF + + @Composable + override fun DisplayBanner() { + DefaultBanner( + title = null, + body = pluralStringResource( + id = R.plurals.GroupsV1MigrationSuggestionsReminder_members_couldnt_be_added_to_the_new_group, + count = suggestionsSize, + suggestionsSize + ), + actions = listOf( + Action(R.plurals.GroupsV1MigrationSuggestionsReminder_add_members, isPluralizedLabel = true, pluralQuantity = suggestionsSize, onAddMembers), + Action(R.string.GroupsV1MigrationSuggestionsReminder_no_thanks, onClick = onNoThanks) + ) + ) + } + + companion object { + + @JvmStatic + fun createFlow(suggestionsSize: Int, onAddMembers: () -> Unit, onNoThanks: () -> Unit): Flow = createAndEmit { + GroupsV1MigrationSuggestionsBanner(suggestionsSize, onAddMembers, onNoThanks) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/OutdatedBuildBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/OutdatedBuildBanner.kt new file mode 100644 index 0000000000..a085686bc6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/OutdatedBuildBanner.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.banner.banners + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import kotlinx.coroutines.flow.Flow +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.ui.compose.Action +import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner +import org.thoughtcrime.securesms.banner.ui.compose.Importance +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.util.PlayStoreUtil +import org.thoughtcrime.securesms.util.Util +import kotlin.time.Duration.Companion.milliseconds + +/** + * Banner to let the user know their build is about to expire or has expired. + */ +class OutdatedBuildBanner(val context: Context, private val daysUntilExpiry: Int) : Banner() { + + override val enabled = SignalStore.misc.isClientDeprecated || daysUntilExpiry <= MAX_DAYS_UNTIL_EXPIRE + + @Composable + override fun DisplayBanner() { + DefaultBanner( + title = null, + body = if (SignalStore.misc.isClientDeprecated) { + stringResource(id = R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today) + } else if (daysUntilExpiry == 0) { + stringResource(id = R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today) + } else { + pluralStringResource(id = R.plurals.OutdatedBuildReminder_your_version_of_signal_will_expire_in_n_days, count = daysUntilExpiry, daysUntilExpiry) + }, + importance = if (SignalStore.misc.isClientDeprecated) { + Importance.ERROR + } else { + Importance.NORMAL + }, + actions = listOf( + Action(R.string.ExpiredBuildReminder_update_now) { + PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context) + } + ) + ) + } + + companion object { + private const val MAX_DAYS_UNTIL_EXPIRE = 10 + + @JvmStatic + fun createFlow(context: Context): Flow = createAndEmit { + val daysUntilExpiry = Util.getTimeUntilBuildExpiry(SignalStore.misc.estimatedServerTime).milliseconds.inWholeDays.toInt() + OutdatedBuildBanner(context, daysUntilExpiry) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/PendingGroupJoinRequestsBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/PendingGroupJoinRequestsBanner.kt new file mode 100644 index 0000000000..206ce14c48 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/PendingGroupJoinRequestsBanner.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.banner.banners + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.pluralStringResource +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flow +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.ui.compose.Action +import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner + +class PendingGroupJoinRequestsBanner(override val enabled: Boolean, private val suggestionsSize: Int, private val onViewClicked: () -> Unit, private val onDismissListener: (() -> Unit)?) : Banner() { + + @Composable + override fun DisplayBanner() { + DefaultBanner( + title = null, + body = pluralStringResource( + id = R.plurals.GroupsV1MigrationSuggestionsReminder_members_couldnt_be_added_to_the_new_group, + count = suggestionsSize, + suggestionsSize + ), + actions = listOf( + Action(R.string.PendingGroupJoinRequestsReminder_view, onClick = onViewClicked) + ), + onDismissListener = onDismissListener + ) + } + + companion object { + + @JvmStatic + fun createFlow(suggestionsSize: Int, onViewClicked: () -> Unit): Flow = Producer(suggestionsSize, onViewClicked).flow + } + + private class Producer(suggestionsSize: Int, onViewClicked: () -> Unit) { + val dismissListener: () -> Unit = { + mutableStateFlow.tryEmit(PendingGroupJoinRequestsBanner(false, suggestionsSize, onViewClicked, null)) + } + private val mutableStateFlow: MutableStateFlow = MutableStateFlow(PendingGroupJoinRequestsBanner(true, suggestionsSize, onViewClicked, dismissListener)) + val flow: Flow = mutableStateFlow + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/ServiceOutageBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/ServiceOutageBanner.kt new file mode 100644 index 0000000000..154129f492 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/ServiceOutageBanner.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.banner.banners + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import kotlinx.coroutines.flow.Flow +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner +import org.thoughtcrime.securesms.banner.ui.compose.Importance +import org.thoughtcrime.securesms.util.TextSecurePreferences + +class ServiceOutageBanner(context: Context) : Banner() { + + override val enabled = TextSecurePreferences.getServiceOutage(context) + + @Composable + override fun DisplayBanner() { + DefaultBanner( + title = null, + body = stringResource(id = R.string.reminder_header_service_outage_text), + importance = Importance.ERROR + ) + } + + companion object { + + @JvmStatic + fun createFlow(context: Context): Flow = createAndEmit { + ServiceOutageBanner(context) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UnauthorizedBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UnauthorizedBanner.kt new file mode 100644 index 0000000000..2919fe27f2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UnauthorizedBanner.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.banner.banners + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import kotlinx.coroutines.flow.Flow +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.ui.compose.Action +import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner +import org.thoughtcrime.securesms.banner.ui.compose.Importance +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.registration.ui.RegistrationActivity +import org.thoughtcrime.securesms.util.TextSecurePreferences + +class UnauthorizedBanner(val context: Context) : Banner() { + + override val enabled = TextSecurePreferences.isUnauthorizedReceived(context) || !SignalStore.account.isRegistered + + @Composable + override fun DisplayBanner() { + DefaultBanner( + title = null, + body = stringResource(id = R.string.UnauthorizedReminder_this_is_likely_because_you_registered_your_phone_number_with_Signal_on_a_different_device), + importance = Importance.ERROR, + actions = listOf( + Action(R.string.UnauthorizedReminder_reregister_action) { + val registrationIntent = RegistrationActivity.newIntentForReRegistration(context) + context.startActivity(registrationIntent) + } + ) + ) + } + + companion object { + + @JvmStatic + fun createFlow(context: Context): Flow = createAndEmit { + UnauthorizedBanner(context) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UsernameOutOfSyncBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UsernameOutOfSyncBanner.kt new file mode 100644 index 0000000000..b93025fd6e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UsernameOutOfSyncBanner.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.banner.banners + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import kotlinx.coroutines.flow.Flow +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.ui.compose.Action +import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner +import org.thoughtcrime.securesms.banner.ui.compose.Importance +import org.thoughtcrime.securesms.keyvalue.AccountValues +import org.thoughtcrime.securesms.keyvalue.AccountValues.UsernameSyncState +import org.thoughtcrime.securesms.keyvalue.SignalStore + +class UsernameOutOfSyncBanner(private val context: Context, private val usernameSyncState: UsernameSyncState, private val onActionClick: (Boolean) -> Unit) : Banner() { + + override val enabled = when (usernameSyncState) { + AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED -> true + AccountValues.UsernameSyncState.LINK_CORRUPTED -> true + AccountValues.UsernameSyncState.IN_SYNC -> false + } + + @Composable + override fun DisplayBanner() { + DefaultBanner( + title = null, + body = if (usernameSyncState == UsernameSyncState.USERNAME_AND_LINK_CORRUPTED) { + stringResource(id = R.string.UsernameOutOfSyncReminder__username_and_link_corrupt) + } else { + stringResource(id = R.string.UsernameOutOfSyncReminder__link_corrupt) + }, + importance = Importance.ERROR, + actions = listOf( + Action(R.string.UsernameOutOfSyncReminder__fix_now) { + onActionClick(usernameSyncState == UsernameSyncState.USERNAME_AND_LINK_CORRUPTED) + } + ) + ) + } + + companion object { + + @JvmStatic + fun createFlow(context: Context, onActionClick: (Boolean) -> Unit): Flow = createAndEmit { + UsernameOutOfSyncBanner(context, SignalStore.account.usernameSyncState, onActionClick) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/ui/compose/DefaultBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/ui/compose/DefaultBanner.kt index 377fe4b7c1..ade6a86122 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/ui/compose/DefaultBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/ui/compose/DefaultBanner.kt @@ -5,8 +5,6 @@ package org.thoughtcrime.securesms.banner.ui.compose -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement @@ -17,7 +15,6 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -30,8 +27,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import org.signal.core.ui.Previews import org.signal.core.ui.SignalPreview import org.signal.core.util.isNotNullOrBlank import org.thoughtcrime.securesms.R @@ -44,11 +43,9 @@ import org.thoughtcrime.securesms.R fun DefaultBanner( title: String?, body: String, - importance: Importance, - isDismissible: Boolean, - onDismissListener: () -> Unit, - onHideListener: () -> Unit, - @DrawableRes icon: Int? = null, + importance: Importance = Importance.NORMAL, + onDismissListener: (() -> Unit)? = null, + onHideListener: (() -> Unit)? = null, actions: List = emptyList(), showProgress: Boolean = false, progressText: String = "", @@ -59,7 +56,7 @@ fun DefaultBanner( .background( color = when (importance) { Importance.NORMAL -> MaterialTheme.colorScheme.surface - Importance.ERROR, Importance.TERMINAL -> colorResource(id = R.color.reminder_background) + Importance.ERROR -> colorResource(id = R.color.reminder_background) } ) .border( @@ -73,22 +70,6 @@ fun DefaultBanner( modifier = Modifier .defaultMinSize(minHeight = 74.dp) ) { - if (icon != null) { - Box( - modifier = Modifier - .padding(horizontal = 12.dp) - .size(48.dp) - .background(MaterialTheme.colorScheme.primaryContainer, CircleShape), - contentAlignment = Alignment.Center - ) { - Icon( - painter = painterResource(id = icon), - contentDescription = stringResource(id = R.string.ReminderView_icon_content_description), - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(22.5.dp) - ) - } - } Column { Row(modifier = Modifier.fillMaxWidth()) { Column( @@ -101,7 +82,7 @@ fun DefaultBanner( text = title, color = when (importance) { Importance.NORMAL -> MaterialTheme.colorScheme.onSurface - Importance.ERROR, Importance.TERMINAL -> colorResource(id = R.color.signal_light_colorOnSurface) + Importance.ERROR -> colorResource(id = R.color.signal_light_colorOnSurface) }, style = MaterialTheme.typography.bodyLarge ) @@ -111,7 +92,7 @@ fun DefaultBanner( text = body, color = when (importance) { Importance.NORMAL -> MaterialTheme.colorScheme.onSurfaceVariant - Importance.ERROR, Importance.TERMINAL -> colorResource(id = R.color.signal_light_colorOnSurface) + Importance.ERROR -> colorResource(id = R.color.signal_light_colorOnSurface) }, style = MaterialTheme.typography.bodyMedium ) @@ -136,16 +117,16 @@ fun DefaultBanner( style = MaterialTheme.typography.bodySmall, color = when (importance) { Importance.NORMAL -> MaterialTheme.colorScheme.onSurfaceVariant - Importance.ERROR, Importance.TERMINAL -> colorResource(id = R.color.signal_light_colorOnSurface) + Importance.ERROR -> colorResource(id = R.color.signal_light_colorOnSurface) } ) } } - if (isDismissible) { + if (onDismissListener != null) { IconButton( onClick = { - onHideListener() + onHideListener?.invoke() onDismissListener() }, modifier = Modifier.size(48.dp) @@ -163,7 +144,13 @@ fun DefaultBanner( ) { for (action in actions) { TextButton(onClick = action.onClick) { - Text(text = stringResource(id = action.label)) + Text( + text = if (!action.isPluralizedLabel) { + stringResource(id = action.label) + } else { + pluralStringResource(id = action.label, count = action.pluralQuantity) + } + ) } } } @@ -172,24 +159,40 @@ fun DefaultBanner( } } -data class Action(@StringRes val label: Int, val onClick: () -> Unit) +data class Action(val label: Int, val isPluralizedLabel: Boolean = false, val pluralQuantity: Int = 0, val onClick: () -> Unit) enum class Importance { - NORMAL, ERROR, TERMINAL + NORMAL, ERROR +} + +@Composable +@SignalPreview +private fun BubblesOptOutPreview() { + Previews.Preview { + DefaultBanner( + title = null, + body = stringResource(id = R.string.BubbleOptOutTooltip__description), + actions = listOf( + Action(R.string.BubbleOptOutTooltip__turn_off) {}, + Action(R.string.BubbleOptOutTooltip__not_now) {} + ) + ) + } } @Composable @SignalPreview private fun ForcedUpgradePreview() { - DefaultBanner( - title = null, - body = stringResource(id = R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today), - importance = Importance.TERMINAL, - isDismissible = false, - actions = listOf(Action(R.string.ExpiredBuildReminder_update_now) {}), - onHideListener = { }, - onDismissListener = {} - ) + Previews.Preview { + DefaultBanner( + title = null, + body = stringResource(id = R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today), + importance = Importance.ERROR, + actions = listOf(Action(R.string.ExpiredBuildReminder_update_now) {}), + onHideListener = { }, + onDismissListener = {} + ) + } } @Composable @@ -199,39 +202,16 @@ private fun FullyLoadedErrorPreview() { Action(R.string.ExpiredBuildReminder_update_now) { }, Action(R.string.BubbleOptOutTooltip__turn_off) { } ) - DefaultBanner( - icon = R.drawable.symbol_error_circle_24, - title = "Error", - body = "Creating more errors.", - importance = Importance.ERROR, - isDismissible = true, - actions = actions, - showProgress = true, - progressText = "4 out of 10 errors created.", - progressPercent = 40, - onHideListener = { }, - onDismissListener = {} - ) -} - -@Composable -@SignalPreview -private fun FullyLoadedTerminalPreview() { - val actions = listOf( - Action(R.string.ExpiredBuildReminder_update_now) { }, - Action(R.string.BubbleOptOutTooltip__turn_off) { } - ) - DefaultBanner( - icon = R.drawable.symbol_error_circle_24, - title = "Terminal", - body = "This is a terminal state.", - importance = Importance.TERMINAL, - isDismissible = true, - actions = actions, - showProgress = true, - progressText = "93% terminated", - progressPercent = 93, - onHideListener = { }, - onDismissListener = {} - ) + Previews.Preview { + DefaultBanner( + title = "Error", + body = "Creating more errors.", + importance = Importance.ERROR, + actions = actions, + showProgress = true, + progressText = "4 out of 10 errors created.", + progressPercent = 40, + onDismissListener = {} + ) + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java index b34f17e124..43d4657db0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/ExpiredBuildReminder.java @@ -1,16 +1,11 @@ package org.thoughtcrime.securesms.components.reminder; import android.content.Context; -import android.os.Build; import androidx.annotation.NonNull; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.util.CommunicationActions; -import org.thoughtcrime.securesms.util.PlayStoreUtil; - -import java.util.List; /** * Showed when a build has fully expired (either via the compile-time constant, or remote diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index fd4e1f2dc3..e8d2585ead 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -97,7 +97,7 @@ import org.thoughtcrime.securesms.badges.self.expired.ExpiredOneTimeBadgeBottomS import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment; import org.thoughtcrime.securesms.banner.Banner; import org.thoughtcrime.securesms.banner.BannerManager; -import org.thoughtcrime.securesms.banner.banners.ExpiredBuildBanner; +import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner; import org.thoughtcrime.securesms.components.DeleteSyncEducationDialog; import org.thoughtcrime.securesms.components.Material3SearchToolbar; import org.thoughtcrime.securesms.components.RatingManager; @@ -882,7 +882,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode private void initializeBanners() { if (RemoteConfig.newBannerUi()) { - final List> bannerRepositories = List.of(ExpiredBuildBanner.createFlow(requireContext())); + final List> bannerRepositories = List.of(OutdatedBuildBanner.createFlow(requireContext())); final BannerManager bannerManager = new BannerManager(bannerRepositories); bannerManager.setContent(bannerView.get()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java index 532dd5eac0..9839a2e42a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/PaymentsHomeFragment.java @@ -10,7 +10,9 @@ import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; +import androidx.compose.ui.platform.ComposeView; import androidx.core.content.ContextCompat; +import androidx.lifecycle.FlowLiveDataConversions; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.Navigation; import androidx.navigation.fragment.NavHostFragment; @@ -25,6 +27,9 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.LoggingFragment; import org.thoughtcrime.securesms.PaymentPreferencesDirections; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.banner.Banner; +import org.thoughtcrime.securesms.banner.BannerManager; +import org.thoughtcrime.securesms.banner.banners.EnclaveFailureBanner; import org.thoughtcrime.securesms.components.reminder.EnclaveFailureReminder; import org.thoughtcrime.securesms.components.reminder.ReminderView; import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity; @@ -40,13 +45,17 @@ import org.thoughtcrime.securesms.payments.preferences.model.PaymentItem; import org.thoughtcrime.securesms.registration.ui.RegistrationActivity; import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.PlayStoreUtil; +import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.util.SpanUtil; import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.navigation.SafeNavigation; import org.thoughtcrime.securesms.util.views.Stub; +import java.util.List; import java.util.concurrent.TimeUnit; +import kotlinx.coroutines.flow.Flow; + public class PaymentsHomeFragment extends LoggingFragment { private static final int DAYS_UNTIL_REPROMPT_PAYMENT_LOCK = 30; private static final int MAX_PAYMENT_LOCK_SKIP_COUNT = 2; @@ -99,6 +108,7 @@ public class PaymentsHomeFragment extends LoggingFragment { View refresh = view.findViewById(R.id.payments_home_fragment_header_refresh); LottieAnimationView refreshAnimation = view.findViewById(R.id.payments_home_fragment_header_refresh_animation); Stub reminderView = ViewUtil.findStubById(view, R.id.reminder); + Stub bannerView = ViewUtil.findStubById(view, R.id.banner_compose_view); toolbar.setNavigationOnClickListener(v -> { viewModel.markAllPaymentsSeen(); @@ -254,22 +264,33 @@ public class PaymentsHomeFragment extends LoggingFragment { } }); - viewModel.getEnclaveFailure().observe(getViewLifecycleOwner(), failure -> { - if (failure) { - showUpdateIsRequiredDialog(); - reminderView.get().showReminder(new EnclaveFailureReminder(requireContext())); - reminderView.get().setOnActionClickListener(actionId -> { - if (actionId == R.id.reminder_action_update_now) { - PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext()); - } else if (actionId == R.id.reminder_action_re_register) { - startActivity(RegistrationActivity.newIntentForReRegistration(requireContext())); - } - }); - } else { - reminderView.get().requestDismiss(); - } - }); - + if (RemoteConfig.newBannerUi()) { + viewModel.getEnclaveFailure().observe(getViewLifecycleOwner(), failure -> { + if (failure) { + showUpdateIsRequiredDialog(); + } + }); + final Flow enclaveFailureFlow = FlowLiveDataConversions.asFlow(viewModel.getEnclaveFailure()); + final List> bannerRepositories = List.of(EnclaveFailureBanner.Companion.mapBooleanFlowToBannerFlow(enclaveFailureFlow, requireContext())); + final BannerManager bannerManager = new BannerManager(bannerRepositories); + bannerManager.setContent(bannerView.get()); + } else { + viewModel.getEnclaveFailure().observe(getViewLifecycleOwner(), failure -> { + if (failure) { + showUpdateIsRequiredDialog(); + reminderView.get().showReminder(new EnclaveFailureReminder(requireContext())); + reminderView.get().setOnActionClickListener(actionId -> { + if (actionId == R.id.reminder_action_update_now) { + PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext()); + } else if (actionId == R.id.reminder_action_re_register) { + startActivity(RegistrationActivity.newIntentForReRegistration(requireContext())); + } + }); + } else { + reminderView.get().requestDismiss(); + } + }); + } requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressed()); } diff --git a/app/src/main/res/layout/payments_home_fragment.xml b/app/src/main/res/layout/payments_home_fragment.xml index f13159865b..81ae58ffad 100644 --- a/app/src/main/res/layout/payments_home_fragment.xml +++ b/app/src/main/res/layout/payments_home_fragment.xml @@ -28,6 +28,15 @@ android:layout="@layout/conversation_list_reminder_view" app:layout_constraintTop_toBottomOf="@id/payments_home_fragment_toolbar" /> + + +