Migrate existing Reminders to Banners.

This commit is contained in:
Nicholas Tinsley
2024-08-07 16:39:29 -04:00
committed by mtang-signal
parent 16a732171a
commit d45acd0e24
17 changed files with 608 additions and 122 deletions

View File

@@ -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.

View File

@@ -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<BubbleOptOutBanner> = createAndEmit {
BubbleOptOutBanner(inBubble, actionListener)
}
}
}

View File

@@ -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<CdsPermanentErrorBanner> = createAndEmit {
CdsPermanentErrorBanner(fragmentManager)
}
}
}

View File

@@ -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<CdsTemporaryErrorBanner> = createAndEmit {
CdsTemporaryErrorBanner(fragmentManager)
}
}
}

View File

@@ -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<DozeBanner> = createAndEmit {
if (Build.VERSION.SDK_INT >= 23) {
DozeBanner(context)
} else {
null
}
}
}
}

View File

@@ -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<Banner> = createAndEmit {
if (SignalStore.misc.isClientDeprecated) {
ExpiredBuildBanner(context)
} else {
null
}
fun Flow<Boolean>.mapBooleanFlowToBannerFlow(context: Context): Flow<EnclaveFailureBanner> {
return map { EnclaveFailureBanner(it, context) }
}
}
}

View File

@@ -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<GroupsV1MigrationSuggestionsBanner> = createAndEmit {
GroupsV1MigrationSuggestionsBanner(suggestionsSize, onAddMembers, onNoThanks)
}
}
}

View File

@@ -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<OutdatedBuildBanner> = createAndEmit {
val daysUntilExpiry = Util.getTimeUntilBuildExpiry(SignalStore.misc.estimatedServerTime).milliseconds.inWholeDays.toInt()
OutdatedBuildBanner(context, daysUntilExpiry)
}
}
}

View File

@@ -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<PendingGroupJoinRequestsBanner> = 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<PendingGroupJoinRequestsBanner> = MutableStateFlow(PendingGroupJoinRequestsBanner(true, suggestionsSize, onViewClicked, dismissListener))
val flow: Flow<PendingGroupJoinRequestsBanner> = mutableStateFlow
}
}

View File

@@ -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<ServiceOutageBanner> = createAndEmit {
ServiceOutageBanner(context)
}
}
}

View File

@@ -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<UnauthorizedBanner> = createAndEmit {
UnauthorizedBanner(context)
}
}
}

View File

@@ -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<UsernameOutOfSyncBanner> = createAndEmit {
UsernameOutOfSyncBanner(context, SignalStore.account.usernameSyncState, onActionClick)
}
}
}

View File

@@ -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<Action> = 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 = {}
)
}
}

View File

@@ -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

View File

@@ -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<Flow<Banner>> bannerRepositories = List.of(ExpiredBuildBanner.createFlow(requireContext()));
final List<Flow<? extends Banner>> bannerRepositories = List.of(OutdatedBuildBanner.createFlow(requireContext()));
final BannerManager bannerManager = new BannerManager(bannerRepositories);
bannerManager.setContent(bannerView.get());
}

View File

@@ -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> reminderView = ViewUtil.findStubById(view, R.id.reminder);
Stub<ComposeView> 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<Boolean> enclaveFailureFlow = FlowLiveDataConversions.asFlow(viewModel.getEnclaveFailure());
final List<Flow<? extends Banner>> 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());
}