Improve the Banner system.

This commit is contained in:
Greyson Parrelli 2024-09-17 14:59:47 -04:00
parent 24133c6dac
commit ba06efe35a
24 changed files with 769 additions and 606 deletions

View file

@ -8,42 +8,30 @@ package org.thoughtcrime.securesms.banner
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import org.signal.core.util.logging.Log
/**
* This class represents a banner across the top of the screen.
*
* Typically, a class will subclass [Banner] and have a nested class that subclasses [BannerFactory].
* The constructor for an implementation of [Banner] should be very lightweight, as it is may be called frequently.
* Banners are submitted to a [BannerManager], which will render the first [enabled] Banner in it's list.
* After a Banner is selected, the [BannerManager] will listen to the [dataFlow] and use the emitted [Model]s to render the [DisplayBanner] composable.
*/
abstract class Banner {
companion object {
private val TAG = Log.tag(Banner::class)
/**
* A helper function to create a [Flow] of a [Banner].
*
* @param bannerFactory a block the produces a [Banner], or null. Returning null will complete the [Flow] without emitting any values.
*/
@JvmStatic
fun <T : Banner> createAndEmit(bannerFactory: () -> T): Flow<T> {
return bannerFactory().let {
flow { emit(it) }
}
}
}
abstract class Banner<Model> {
/**
* Whether or not the [Banner] should be shown (enabled) or hidden (disabled).
* Whether or not the [Banner] is eligible for display. This is read on the main thread and therefore should be very fast.
*/
abstract val enabled: Boolean
/**
* Composable function to display content when [enabled] is true.
*
* @see [org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner]
* A [Flow] that emits the model to be displayed in the [DisplayBanner] composable.
* This flow will only be subscribed to if the banner is [enabled].
*/
abstract val dataFlow: Flow<Model>
/**
* Composable function to display the content emitted from [dataFlow].
* You likely want to use [org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner].
*/
@Composable
abstract fun DisplayBanner(contentPadding: PaddingValues)
abstract fun DisplayBanner(model: Model, contentPadding: PaddingValues)
}

View file

@ -7,12 +7,13 @@ package org.thoughtcrime.securesms.banner
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.logging.Log
@ -21,7 +22,7 @@ import org.signal.core.util.logging.Log
* Usually, the [Flow]s will come from [Banner.BannerFactory] instances, but may also be produced by the other properties of the host.
*/
class BannerManager @JvmOverloads constructor(
allFlows: Iterable<Flow<Banner>>,
private val banners: List<Banner<*>>,
private val onNewBannerShownListener: () -> Unit = {},
private val onNoBannerShownListener: () -> Unit = {}
) {
@ -31,35 +32,31 @@ class BannerManager @JvmOverloads constructor(
}
/**
* Takes the flows and combines them into one so that a new [Flow] value from any of them will trigger an update to the UI.
*
* **NOTE**: This will **not** emit its first value until **all** of the input flows have each emitted *at least one value*.
* Re-evaluates the [Banner]s, choosing one to render (if any) and updating the view.
*/
private val combinedFlow: Flow<List<Banner>> = combine(allFlows) { banners: Array<Banner> ->
banners.filter { it.enabled }.toList()
}
fun updateContent(composeView: ComposeView) {
val banner: Banner<Any>? = banners.firstOrNull { it.enabled } as Banner<Any>?
/**
* Sets the content of the provided [ComposeView] to one that consumes the lists emitted by [combinedFlow] and displays them.
*/
fun setContent(composeView: ComposeView) {
composeView.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val state = combinedFlow.collectAsStateWithLifecycle(initialValue = emptyList())
if (banner == null) {
onNoBannerShownListener()
return@setContent
}
val bannerToDisplay = state.value.firstOrNull()
if (bannerToDisplay != null) {
val state: State<Any?> = banner.dataFlow.collectAsStateWithLifecycle(initialValue = null)
val bannerState by state
bannerState?.let { model ->
SignalTheme {
Box {
bannerToDisplay.DisplayBanner(PaddingValues(horizontal = 12.dp, vertical = 8.dp))
banner.DisplayBanner(model, PaddingValues(horizontal = 12.dp, vertical = 8.dp))
}
}
onNewBannerShownListener()
} else {
onNoBannerShownListener()
}
} ?: onNoBannerShownListener()
}
}
}

View file

@ -1,24 +0,0 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.banner
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
abstract class DismissibleBannerProducer<T : Banner>(bannerProducer: (dismissListener: () -> Unit) -> T) {
abstract fun createDismissedBanner(): T
private val mutableSharedFlow: MutableSharedFlow<T> = MutableSharedFlow(replay = 1)
private val dismissListener = {
mutableSharedFlow.tryEmit(createDismissedBanner())
}
init {
mutableSharedFlow.tryEmit(bannerProducer(dismissListener))
}
val flow: Flow<T> = mutableSharedFlow
}

View file

@ -9,49 +9,51 @@ import android.os.Build
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.DismissibleBannerProducer
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() {
class BubbleOptOutBanner(private val inBubble: Boolean, private val actionListener: (Boolean) -> Unit) : Banner<Unit>() {
override val enabled: Boolean = inBubble && !SignalStore.tooltips.hasSeenBubbleOptOutTooltip() && Build.VERSION.SDK_INT > 29
override val enabled: Boolean
get() = inBubble && !SignalStore.tooltips.hasSeenBubbleOptOutTooltip() && Build.VERSION.SDK_INT > 29
override val dataFlow: Flow<Unit>
get() = flowOf(Unit)
@Composable
override fun DisplayBanner(contentPadding: PaddingValues) {
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)
}
),
paddingValues = contentPadding
)
}
override fun DisplayBanner(model: Unit, contentPadding: PaddingValues) = Banner(contentPadding, actionListener)
}
private class Producer(inBubble: Boolean, actionListener: (Boolean) -> Unit) : DismissibleBannerProducer<BubbleOptOutBanner>(bannerProducer = {
BubbleOptOutBanner(inBubble) { turnOffBubbles ->
actionListener(turnOffBubbles)
it()
}
}) {
override fun createDismissedBanner(): BubbleOptOutBanner {
return BubbleOptOutBanner(false) {}
}
}
@Composable
private fun Banner(contentPadding: PaddingValues, actionListener: (Boolean) -> Unit = {}) {
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)
}
),
paddingValues = contentPadding
)
}
companion object {
fun createFlow(inBubble: Boolean, actionListener: (Boolean) -> Unit): Flow<BubbleOptOutBanner> {
return Producer(inBubble, actionListener).flow
}
@SignalPreview
@Composable
private fun BannerPreview() {
Previews.Preview {
Banner(PaddingValues(0.dp))
}
}

View file

@ -8,8 +8,11 @@ package org.thoughtcrime.securesms.banner.banners
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@ -19,37 +22,53 @@ 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(contentPadding: PaddingValues) {
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)
}
),
paddingValues = contentPadding
)
}
class CdsPermanentErrorBanner(private val fragmentManager: FragmentManager) : Banner<Unit>() {
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(childFragmentManager: FragmentManager): Flow<CdsPermanentErrorBanner> = createAndEmit {
CdsPermanentErrorBanner(childFragmentManager)
override val enabled: Boolean
get() {
val timeUntilUnblock = SignalStore.misc.cdsBlockedUtil - System.currentTimeMillis()
return SignalStore.misc.isCdsBlocked && timeUntilUnblock >= PERMANENT_TIME_CUTOFF
}
override val dataFlow
get() = flowOf(Unit)
@Composable
override fun DisplayBanner(model: Unit, contentPadding: PaddingValues) {
Banner(
contentPadding = contentPadding,
onLearnMoreClicked = { CdsPermanentErrorBottomSheet.show(fragmentManager) }
)
}
}
@Composable
private fun Banner(contentPadding: PaddingValues, onLearnMoreClicked: () -> Unit = {}) {
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) {
onLearnMoreClicked()
}
),
paddingValues = contentPadding
)
}
@SignalPreview
@Composable
private fun BannerPreview() {
Previews.Preview {
Banner(PaddingValues(0.dp))
}
}

View file

@ -8,8 +8,11 @@ package org.thoughtcrime.securesms.banner.banners
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentManager
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@ -18,31 +21,45 @@ 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()
class CdsTemporaryErrorBanner(private val fragmentManager: FragmentManager) : Banner<Unit>() {
override val enabled: Boolean = SignalStore.misc.isCdsBlocked && timeUntilUnblock < CdsPermanentErrorBanner.PERMANENT_TIME_CUTOFF
override val enabled: Boolean
get() {
val timeUntilUnblock = SignalStore.misc.cdsBlockedUtil - System.currentTimeMillis()
return SignalStore.misc.isCdsBlocked && timeUntilUnblock < CdsPermanentErrorBanner.PERMANENT_TIME_CUTOFF
}
override val dataFlow
get() = flowOf(Unit)
@Composable
override fun DisplayBanner(contentPadding: PaddingValues) {
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)
}
),
paddingValues = contentPadding
override fun DisplayBanner(model: Unit, contentPadding: PaddingValues) {
Banner(
contentPadding = contentPadding,
onLearnMoreClicked = { CdsTemporaryErrorBottomSheet.show(fragmentManager) }
)
}
}
companion object {
@Composable
private fun Banner(contentPadding: PaddingValues, onLearnMoreClicked: () -> Unit = {}) {
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) {
onLearnMoreClicked()
}
),
paddingValues = contentPadding
)
}
@JvmStatic
fun createFlow(childFragmentManager: FragmentManager): Flow<CdsTemporaryErrorBanner> = createAndEmit {
CdsTemporaryErrorBanner(childFragmentManager)
}
@SignalPreview
@Composable
private fun BannerPreview() {
Previews.Preview {
Banner(PaddingValues(0.dp))
}
}

View file

@ -0,0 +1,69 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.banner.banners
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
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
/**
* Shown when a build is actively deprecated and unable to connect to the service.
*/
class DeprecatedBuildBanner : Banner<Unit>() {
override val enabled: Boolean
get() = SignalStore.misc.isClientDeprecated
override val dataFlow: Flow<Unit>
get() = flowOf(Unit)
@Composable
override fun DisplayBanner(model: Unit, contentPadding: PaddingValues) {
val context = LocalContext.current
Banner(
contentPadding = contentPadding,
onUpdateClicked = {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context)
}
)
}
}
@Composable
private fun Banner(contentPadding: PaddingValues, onUpdateClicked: () -> Unit = {}) {
DefaultBanner(
title = null,
body = stringResource(id = R.string.ExpiredBuildReminder_this_version_of_signal_has_expired),
importance = Importance.ERROR,
actions = listOf(
Action(R.string.ExpiredBuildReminder_update_now) {
onUpdateClicked()
}
),
paddingValues = contentPadding
)
}
@SignalPreview
@Composable
private fun BannerPreview() {
Previews.Preview {
Banner(contentPadding = PaddingValues(0.dp))
}
}

View file

@ -10,10 +10,13 @@ import android.os.Build
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.DismissibleBannerProducer
import org.thoughtcrime.securesms.banner.ui.compose.Action
import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
import org.thoughtcrime.securesms.keyvalue.SignalStore
@ -21,44 +24,52 @@ import org.thoughtcrime.securesms.util.PowerManagerCompat
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
class DozeBanner(private val context: Context, val dismissed: Boolean, private val onDismiss: () -> Unit) : Banner() {
override val enabled: Boolean = !dismissed &&
Build.VERSION.SDK_INT >= 23 && !SignalStore.account.fcmEnabled && !TextSecurePreferences.hasPromptedOptimizeDoze(context) && !ServiceUtil.getPowerManager(context).isIgnoringBatteryOptimizations(context.packageName)
class DozeBanner(private val context: Context) : Banner<Unit>() {
override val enabled: Boolean
get() = Build.VERSION.SDK_INT >= 23 && !SignalStore.account.fcmEnabled && !TextSecurePreferences.hasPromptedOptimizeDoze(context) && !ServiceUtil.getPowerManager(context).isIgnoringBatteryOptimizations(context.packageName)
override val dataFlow: Flow<Unit>
get() = flowOf(Unit)
@Composable
override fun DisplayBanner(contentPadding: PaddingValues) {
override fun DisplayBanner(model: Unit, contentPadding: PaddingValues) {
if (Build.VERSION.SDK_INT < 23) {
throw IllegalStateException("Showing a Doze banner for an OS prior to Android 6.0")
}
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),
Banner(
contentPadding = contentPadding,
onDismissListener = {
TextSecurePreferences.setPromptedOptimizeDoze(context, true)
onDismiss()
},
actions = listOf(
Action(android.R.string.ok) {
TextSecurePreferences.setPromptedOptimizeDoze(context, true)
PowerManagerCompat.requestIgnoreBatteryOptimizations(context)
}
),
paddingValues = contentPadding
onOkListener = {
TextSecurePreferences.setPromptedOptimizeDoze(context, true)
PowerManagerCompat.requestIgnoreBatteryOptimizations(context)
}
)
}
}
private class Producer(private val context: Context) : DismissibleBannerProducer<DozeBanner>(bannerProducer = {
DozeBanner(context = context, dismissed = false, onDismiss = it)
}) {
override fun createDismissedBanner(): DozeBanner {
return DozeBanner(context, true) {}
}
}
@Composable
private fun Banner(contentPadding: PaddingValues, onDismissListener: () -> Unit = {}, onOkListener: () -> Unit = {}) {
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),
onDismissListener = onDismissListener,
actions = listOf(
Action(android.R.string.ok) {
onOkListener()
}
),
paddingValues = contentPadding
)
}
companion object {
@JvmStatic
fun createFlow(context: Context): Flow<DozeBanner> {
return Producer(context).flow
}
@SignalPreview
@Composable
private fun BannerPreview() {
Previews.Preview {
Banner(contentPadding = PaddingValues(0.dp))
}
}

View file

@ -5,12 +5,15 @@
package org.thoughtcrime.securesms.banner.banners
import android.content.Context
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@ -18,28 +21,45 @@ import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
import org.thoughtcrime.securesms.banner.ui.compose.Importance
import org.thoughtcrime.securesms.util.PlayStoreUtil
class EnclaveFailureBanner(enclaveFailed: Boolean, private val context: Context) : Banner() {
override val enabled: Boolean = enclaveFailed
class EnclaveFailureBanner(private val enclaveFailed: Boolean) : Banner<Unit>() {
override val enabled: Boolean
get() = enclaveFailed
override val dataFlow: Flow<Unit>
get() = flowOf(Unit)
@Composable
override fun DisplayBanner(contentPadding: PaddingValues) {
DefaultBanner(
title = null,
body = stringResource(id = R.string.EnclaveFailureReminder_update_signal),
importance = Importance.ERROR,
actions = listOf(
Action(R.string.ExpiredBuildReminder_update_now) {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context)
}
),
paddingValues = contentPadding
override fun DisplayBanner(model: Unit, contentPadding: PaddingValues) {
val context = LocalContext.current
Banner(
contentPadding = contentPadding,
onUpdateNow = {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context)
}
)
}
}
companion object {
@JvmStatic
fun Flow<Boolean>.mapBooleanFlowToBannerFlow(context: Context): Flow<EnclaveFailureBanner> {
return map { EnclaveFailureBanner(it, context) }
}
@Composable
private fun Banner(contentPadding: PaddingValues, onUpdateNow: () -> Unit = {}) {
DefaultBanner(
title = null,
body = stringResource(id = R.string.EnclaveFailureReminder_update_signal),
importance = Importance.ERROR,
actions = listOf(
Action(R.string.ExpiredBuildReminder_update_now) {
onUpdateNow()
}
),
paddingValues = contentPadding
)
}
@SignalPreview
@Composable
private fun BannerPreview() {
Previews.Preview {
Banner(contentPadding = PaddingValues(0.dp))
}
}

View file

@ -8,50 +8,72 @@ package org.thoughtcrime.securesms.banner.banners
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.DismissibleBannerProducer
import org.thoughtcrime.securesms.banner.ui.compose.Action
import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
class GroupsV1MigrationSuggestionsBanner(private val suggestionsSize: Int, private val onAddMembers: () -> Unit, private val onNoThanks: () -> Unit) : Banner() {
override val enabled: Boolean = suggestionsSize > 0
/**
* After migrating a group from v1 -> v2, this banner is used to show suggestions for members to add who couldn't be added automatically.
* Intended to be shown only in a conversation.
*/
class GroupsV1MigrationSuggestionsBanner(
private val suggestionsSize: Int,
private val onAddMembers: () -> Unit,
private val onNoThanks: () -> Unit
) : Banner<Int>() {
override val enabled: Boolean
get() = suggestionsSize > 0
override val dataFlow: Flow<Int>
get() = flowOf(suggestionsSize)
@Composable
override fun DisplayBanner(contentPadding: PaddingValues) {
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)
),
paddingValues = contentPadding
override fun DisplayBanner(model: Int, contentPadding: PaddingValues) {
Banner(
contentPadding = contentPadding,
suggestionsSize = model,
onAddMembers = onAddMembers,
onNoThanks = onNoThanks
)
}
}
private class Producer(suggestionsSize: Int, onAddMembers: () -> Unit, onNoThanks: () -> Unit) : DismissibleBannerProducer<GroupsV1MigrationSuggestionsBanner>(bannerProducer = {
GroupsV1MigrationSuggestionsBanner(
suggestionsSize,
onAddMembers
) {
onNoThanks()
it()
}
}) {
override fun createDismissedBanner(): GroupsV1MigrationSuggestionsBanner {
return GroupsV1MigrationSuggestionsBanner(0, {}, {})
}
}
@Composable
private fun Banner(contentPadding: PaddingValues, suggestionsSize: Int, onAddMembers: () -> Unit = {}, onNoThanks: () -> Unit = {}) {
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)
),
paddingValues = contentPadding
)
}
companion object {
fun createFlow(suggestionsSize: Int, onAddMembers: () -> Unit, onNoThanks: () -> Unit): Flow<GroupsV1MigrationSuggestionsBanner> {
return Producer(suggestionsSize, onAddMembers, onNoThanks).flow
}
@SignalPreview
@Composable
private fun BannerPreviewSingular() {
Previews.Preview {
Banner(contentPadding = PaddingValues(0.dp), suggestionsSize = 1)
}
}
@SignalPreview
@Composable
private fun BannerPreviewPlural() {
Previews.Preview {
Banner(contentPadding = PaddingValues(0.dp), suggestionsSize = 2)
}
}

View file

@ -7,16 +7,13 @@ package org.thoughtcrime.securesms.banner.banners
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.flowWithLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import org.signal.core.util.throttleLatest
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatus
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
@ -27,29 +24,18 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import kotlin.time.Duration.Companion.seconds
class MediaRestoreProgressBanner(private val data: MediaRestoreEvent) : Banner() {
class MediaRestoreProgressBanner : Banner<BackupStatusData>() {
companion object {
/**
* Create a Lifecycle-aware [Flow] of [MediaRestoreProgressBanner] that observes the database for changes in attachments and emits banners when attachments are updated.
*/
@JvmStatic
fun createLifecycleAwareFlow(lifecycleOwner: LifecycleOwner): Flow<MediaRestoreProgressBanner> {
return if (SignalStore.backup.isRestoreInProgress) {
restoreFlow(lifecycleOwner)
} else {
flow {
emit(MediaRestoreProgressBanner(MediaRestoreEvent(0L, 0L)))
}
override val enabled: Boolean
get() = SignalStore.backup.isRestoreInProgress
override val dataFlow: Flow<BackupStatusData>
get() {
if (!SignalStore.backup.isRestoreInProgress) {
return flowOf(BackupStatusData.RestoringMedia(0, 0))
}
}
/**
* Create a flow that listens for all attachment changes in the db and emits a new banner at most
* once every 1 second.
*/
private fun restoreFlow(lifecycleOwner: LifecycleOwner): Flow<MediaRestoreProgressBanner> {
val flow = callbackFlow {
val dbNotificationFlow = callbackFlow {
val queryObserver = DatabaseObserver.Observer {
trySend(Unit)
}
@ -62,29 +48,20 @@ class MediaRestoreProgressBanner(private val data: MediaRestoreEvent) : Banner()
}
}
return flow
.flowWithLifecycle(lifecycleOwner.lifecycle)
return dbNotificationFlow
.throttleLatest(1.seconds)
.map { MediaRestoreProgressBanner(loadData()) }
.map {
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
val completedBytes = totalRestoreSize - remainingAttachmentSize
BackupStatusData.RestoringMedia(completedBytes, totalRestoreSize)
}
.flowOn(Dispatchers.IO)
}
private suspend fun loadData() = withContext(Dispatchers.IO) {
// TODO [backups]: define and query data for interrupted/paused restores
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
val completedBytes = totalRestoreSize - remainingAttachmentSize
MediaRestoreEvent(completedBytes, totalRestoreSize)
}
}
override var enabled: Boolean = data.totalBytes > 0L && data.totalBytes != data.completedBytes
@Composable
override fun DisplayBanner(contentPadding: PaddingValues) {
BackupStatus(data = BackupStatusData.RestoringMedia(data.completedBytes, data.totalBytes))
override fun DisplayBanner(model: BackupStatusData, contentPadding: PaddingValues) {
BackupStatus(data = model)
}
data class MediaRestoreEvent(val completedBytes: Long, val totalBytes: Long)
}

View file

@ -5,12 +5,16 @@
package org.thoughtcrime.securesms.banner.banners
import android.content.Context
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@ -22,72 +26,96 @@ 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.
*
* @param status can be used to filter which conditions are shown.
* Banner to let the user know their build is about to expire.
*/
class OutdatedBuildBanner(val context: Context, private val daysUntilExpiry: Int, private val status: ExpiryStatus) : Banner() {
override val enabled = when (status) {
ExpiryStatus.OUTDATED_ONLY -> SignalStore.misc.isClientDeprecated
ExpiryStatus.EXPIRED_ONLY -> daysUntilExpiry <= MAX_DAYS_UNTIL_EXPIRE
ExpiryStatus.OUTDATED_OR_EXPIRED -> SignalStore.misc.isClientDeprecated || daysUntilExpiry <= MAX_DAYS_UNTIL_EXPIRE
}
@Composable
override fun DisplayBanner(contentPadding: PaddingValues) {
val bodyText = when (status) {
ExpiryStatus.OUTDATED_ONLY -> 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)
}
ExpiryStatus.EXPIRED_ONLY -> stringResource(id = R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today)
ExpiryStatus.OUTDATED_OR_EXPIRED -> 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)
}
}
DefaultBanner(
title = null,
body = bodyText,
importance = if (SignalStore.misc.isClientDeprecated) {
Importance.ERROR
} else {
Importance.NORMAL
},
actions = listOf(
Action(R.string.ExpiredBuildReminder_update_now) {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context)
}
),
paddingValues = contentPadding
)
}
/**
* A enumeration for [OutdatedBuildBanner] to limit it to showing either [OUTDATED_ONLY] status, [EXPIRED_ONLY] status, or both.
*
* [OUTDATED_ONLY] refers to builds that are still valid but need to be updated.
* [EXPIRED_ONLY] refers to builds that are no longer allowed to connect to the service.
*/
enum class ExpiryStatus {
OUTDATED_ONLY,
EXPIRED_ONLY,
OUTDATED_OR_EXPIRED
}
class OutdatedBuildBanner : Banner<Int>() {
companion object {
private const val MAX_DAYS_UNTIL_EXPIRE = 10
}
@JvmStatic
fun createFlow(context: Context, status: ExpiryStatus): Flow<OutdatedBuildBanner> = createAndEmit {
override val enabled: Boolean
get() {
val daysUntilExpiry = Util.getTimeUntilBuildExpiry(SignalStore.misc.estimatedServerTime).milliseconds.inWholeDays.toInt()
OutdatedBuildBanner(context, daysUntilExpiry, status)
return daysUntilExpiry <= MAX_DAYS_UNTIL_EXPIRE
}
override val dataFlow: Flow<Int>
get() = flowOf(Util.getTimeUntilBuildExpiry(SignalStore.misc.estimatedServerTime).milliseconds.inWholeDays.toInt())
@Composable
override fun DisplayBanner(model: Int, contentPadding: PaddingValues) {
val context = LocalContext.current
Banner(
contentPadding = contentPadding,
daysUntilExpiry = model,
onUpdateClicked = {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context)
}
)
}
data class Model(
val daysUntilExpiry: Int,
val isClientDeprecated: Boolean
)
}
@Composable
private fun Banner(contentPadding: PaddingValues, daysUntilExpiry: Int, onUpdateClicked: () -> Unit = {}) {
val bodyText = 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)
}
DefaultBanner(
title = null,
body = bodyText,
importance = if (daysUntilExpiry == 0) {
Importance.ERROR
} else {
Importance.NORMAL
},
actions = listOf(
Action(R.string.ExpiredBuildReminder_update_now) {
onUpdateClicked()
}
),
paddingValues = contentPadding
)
}
@SignalPreview
@Composable
private fun BannerPreviewExpireToday() {
Previews.Preview {
Banner(
contentPadding = PaddingValues(0.dp),
daysUntilExpiry = 0
)
}
}
@SignalPreview
@Composable
private fun BannerPreviewExpireTomorrow() {
Previews.Preview {
Banner(
contentPadding = PaddingValues(0.dp),
daysUntilExpiry = 1
)
}
}
@SignalPreview
@Composable
private fun BannerPreviewExpireLater() {
Previews.Preview {
Banner(
contentPadding = PaddingValues(0.dp),
daysUntilExpiry = 3
)
}
}

View file

@ -7,44 +7,77 @@ package org.thoughtcrime.securesms.banner.banners
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.DismissibleBannerProducer
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() {
/**
* Shows the number of pending requests to join the group.
* Intended to be shown at the top of a conversation.
*/
class PendingGroupJoinRequestsBanner(private val suggestionsSize: Int, private val onViewClicked: () -> Unit) : Banner<Int>() {
override val enabled: Boolean
get() = suggestionsSize > 0
override val dataFlow: Flow<Int> = flowOf(suggestionsSize)
@Composable
override fun DisplayBanner(contentPadding: PaddingValues) {
DefaultBanner(
title = null,
body = pluralStringResource(
id = R.plurals.PendingGroupJoinRequestsReminder_d_pending_member_requests,
count = suggestionsSize,
suggestionsSize
),
onDismissListener = onDismissListener,
actions = listOf(
Action(R.string.PendingGroupJoinRequestsReminder_view, onClick = onViewClicked)
),
paddingValues = contentPadding
override fun DisplayBanner(model: Int, contentPadding: PaddingValues) {
Banner(
contentPadding = contentPadding,
suggestionsSize = model,
onViewClicked = onViewClicked
)
}
}
private class Producer(suggestionsSize: Int, onViewClicked: () -> Unit) : DismissibleBannerProducer<PendingGroupJoinRequestsBanner>(bannerProducer = {
PendingGroupJoinRequestsBanner(suggestionsSize > 0, suggestionsSize, onViewClicked, it)
}) {
override fun createDismissedBanner(): PendingGroupJoinRequestsBanner {
return PendingGroupJoinRequestsBanner(false, 0, {}, null)
}
@Composable
private fun Banner(contentPadding: PaddingValues, suggestionsSize: Int, onViewClicked: () -> Unit = {}) {
var visible by remember { mutableStateOf(true) }
if (!visible) {
return
}
companion object {
fun createFlow(suggestionsSize: Int, onViewClicked: () -> Unit): Flow<PendingGroupJoinRequestsBanner> {
return Producer(suggestionsSize, onViewClicked).flow
}
DefaultBanner(
title = null,
body = pluralStringResource(
id = R.plurals.PendingGroupJoinRequestsReminder_d_pending_member_requests,
count = suggestionsSize,
suggestionsSize
),
onDismissListener = { visible = false },
actions = listOf(
Action(R.string.PendingGroupJoinRequestsReminder_view, onClick = onViewClicked)
),
paddingValues = contentPadding
)
}
@SignalPreview
@Composable
private fun BannerPreviewSingular() {
Previews.Preview {
Banner(contentPadding = PaddingValues(0.dp), suggestionsSize = 1)
}
}
@SignalPreview
@Composable
private fun BannerPreviewPlural() {
Previews.Preview {
Banner(contentPadding = PaddingValues(0.dp), suggestionsSize = 2)
}
}

View file

@ -9,50 +9,42 @@ import android.content.Context
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.map
import org.signal.core.util.logging.Log
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
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(outageInProgress: Boolean) : Banner() {
class ServiceOutageBanner(val context: Context) : Banner<Unit>() {
constructor(context: Context) : this(TextSecurePreferences.getServiceOutage(context))
override val enabled: Boolean
get() = TextSecurePreferences.getServiceOutage(context)
override val enabled = outageInProgress
override val dataFlow: Flow<Unit> = flowOf(Unit)
@Composable
override fun DisplayBanner(contentPadding: PaddingValues) {
DefaultBanner(
title = null,
body = stringResource(id = R.string.reminder_header_service_outage_text),
importance = Importance.ERROR,
paddingValues = contentPadding
)
}
override fun DisplayBanner(model: Unit, contentPadding: PaddingValues) = Banner(contentPadding)
}
/**
* A class that can be held by a listener but still produce new [ServiceOutageBanner] in its flow.
* Designed for being called upon by a listener that is listening to changes in [TextSecurePreferences]
*/
class Producer(private val context: Context) {
private val _flow = MutableSharedFlow<Boolean>(replay = 1)
val flow: Flow<ServiceOutageBanner> = _flow.map { ServiceOutageBanner(context) }
@Composable
private fun Banner(contentPadding: PaddingValues) {
DefaultBanner(
title = null,
body = stringResource(id = R.string.reminder_header_service_outage_text),
importance = Importance.ERROR,
paddingValues = contentPadding
)
}
init {
queryAndEmit()
}
fun queryAndEmit() {
_flow.tryEmit(TextSecurePreferences.getServiceOutage(context))
}
}
companion object {
private val TAG = Log.tag(ServiceOutageBanner::class)
@SignalPreview
@Composable
private fun BannerPreview() {
Previews.Preview {
Banner(contentPadding = PaddingValues(0.dp))
}
}

View file

@ -8,11 +8,13 @@ package org.thoughtcrime.securesms.banner.banners
import android.content.Context
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.map
import org.signal.core.util.logging.Log
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@ -25,44 +27,42 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences
/**
* A banner displayed when the client is unauthorized (deregistered).
*/
class UnauthorizedBanner(val context: Context) : Banner() {
class UnauthorizedBanner(val context: Context) : Banner<Unit>() {
override val enabled = TextSecurePreferences.isUnauthorizedReceived(context) || !SignalStore.account.isRegistered
override val enabled: Boolean
get() = TextSecurePreferences.isUnauthorizedReceived(context) || !SignalStore.account.isRegistered
override val dataFlow: Flow<Unit>
get() = flowOf(Unit)
@Composable
override fun DisplayBanner(contentPadding: PaddingValues) {
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)
}
),
paddingValues = contentPadding
)
}
/**
* A class that can be held by a listener but still produce new [UnauthorizedBanner] in its flow.
* Designed for being called upon by a listener that is listening to changes in [TextSecurePreferences]
*/
class Producer(private val context: Context) {
private val _flow = MutableSharedFlow<Boolean>(replay = 1)
val flow: Flow<UnauthorizedBanner> = _flow.map { UnauthorizedBanner(context) }
init {
queryAndEmit()
}
fun queryAndEmit() {
_flow.tryEmit(TextSecurePreferences.isUnauthorizedReceived(context))
}
}
companion object {
private val TAG = Log.tag(UnauthorizedBanner::class)
override fun DisplayBanner(model: Unit, contentPadding: PaddingValues) {
Banner(contentPadding)
}
}
@Composable
private fun Banner(contentPadding: PaddingValues) {
val context = LocalContext.current
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)
}
),
paddingValues = contentPadding
)
}
@SignalPreview
@Composable
private fun BannerPreview() {
Previews.Preview {
Banner(PaddingValues(0.dp))
}
}

View file

@ -5,11 +5,14 @@
package org.thoughtcrime.securesms.banner.banners
import android.content.Context
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@ -19,41 +22,61 @@ 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() {
class UsernameOutOfSyncBanner(private val onActionClick: (UsernameSyncState) -> Unit) : Banner<UsernameSyncState>() {
override val enabled = when (usernameSyncState) {
AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED -> true
AccountValues.UsernameSyncState.LINK_CORRUPTED -> true
AccountValues.UsernameSyncState.IN_SYNC -> false
}
override val enabled: Boolean
get() {
return when (SignalStore.account.usernameSyncState) {
AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED -> true
AccountValues.UsernameSyncState.LINK_CORRUPTED -> true
AccountValues.UsernameSyncState.IN_SYNC -> false
}
}
override val dataFlow: Flow<UsernameSyncState>
get() = flowOf(SignalStore.account.usernameSyncState)
@Composable
override fun DisplayBanner(contentPadding: PaddingValues) {
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)
}
),
paddingValues = contentPadding
override fun DisplayBanner(model: UsernameSyncState, contentPadding: PaddingValues) {
Banner(
contentPadding = contentPadding,
usernameSyncState = model,
onFixClicked = onActionClick
)
}
}
companion object {
@Composable
private fun Banner(contentPadding: PaddingValues, usernameSyncState: UsernameSyncState, onFixClicked: (UsernameSyncState) -> Unit = {}) {
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) {
onFixClicked(usernameSyncState)
}
),
paddingValues = contentPadding
)
}
/**
* @param onActionClick input is true if both the username and the link are corrupted, false if only the link is corrupted
*/
@JvmStatic
fun createFlow(context: Context, onActionClick: (Boolean) -> Unit): Flow<UsernameOutOfSyncBanner> = createAndEmit {
UsernameOutOfSyncBanner(context, SignalStore.account.usernameSyncState, onActionClick)
}
@SignalPreview
@Composable
private fun BannerPreviewUsernameCorrupted() {
Previews.Preview {
Banner(contentPadding = PaddingValues(0.dp), usernameSyncState = UsernameSyncState.USERNAME_AND_LINK_CORRUPTED)
}
}
@SignalPreview
@Composable
private fun BannerPreviewLinkCorrupted() {
Previews.Preview {
Banner(contentPadding = PaddingValues(0.dp), usernameSyncState = UsernameSyncState.LINK_CORRUPTED)
}
}

View file

@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
@ -106,7 +107,10 @@ fun DefaultBanner(
if (progressPercent >= 0) {
LinearProgressIndicator(
progress = { progressPercent / 100f },
color = MaterialTheme.colorScheme.primary,
color = when (importance) {
Importance.NORMAL -> MaterialTheme.colorScheme.primary
Importance.ERROR -> colorResource(id = R.color.signal_light_colorPrimary)
},
trackColor = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier
.padding(vertical = 12.dp)
@ -114,20 +118,23 @@ fun DefaultBanner(
)
} else {
LinearProgressIndicator(
color = MaterialTheme.colorScheme.primary,
color = when (importance) {
Importance.NORMAL -> MaterialTheme.colorScheme.primary
Importance.ERROR -> colorResource(id = R.color.signal_light_colorPrimary)
},
trackColor = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier.padding(vertical = 12.dp)
)
}
Text(
text = progressText,
style = MaterialTheme.typography.bodySmall,
color = when (importance) {
Importance.NORMAL -> MaterialTheme.colorScheme.onSurfaceVariant
Importance.ERROR -> colorResource(id = R.color.signal_light_colorOnSurface)
}
)
}
Text(
text = progressText,
style = MaterialTheme.typography.bodySmall,
color = when (importance) {
Importance.NORMAL -> MaterialTheme.colorScheme.onSurfaceVariant
Importance.ERROR -> colorResource(id = R.color.signal_light_colorOnSurface)
}
)
}
Box(modifier = Modifier.size(48.dp)) {
@ -154,7 +161,13 @@ fun DefaultBanner(
.padding(end = 8.dp)
) {
for (action in actions) {
TextButton(onClick = action.onClick) {
TextButton(
onClick = action.onClick,
colors = when (importance) {
Importance.NORMAL -> ButtonDefaults.textButtonColors()
Importance.ERROR -> ButtonDefaults.textButtonColors(contentColor = colorResource(R.color.signal_light_colorPrimary))
}
) {
Text(
text = if (!action.isPluralizedLabel) {
stringResource(id = action.label)

View file

@ -16,7 +16,7 @@ import org.signal.core.util.isNotNullOrBlank
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.banner.BannerManager
import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner
import org.thoughtcrime.securesms.banner.banners.DeprecatedBuildBanner
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
@ -35,8 +35,6 @@ import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.Environment
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.SharedPreferencesLifecycleObserver
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
@ -53,6 +51,7 @@ class AppSettingsFragment : DSLSettingsFragment(
private val viewModel: AppSettingsViewModel by viewModels()
private var bannerManager: BannerManager? = null
private lateinit var bannerView: Stub<ComposeView>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -61,7 +60,7 @@ class AppSettingsFragment : DSLSettingsFragment(
super.onViewCreated(view, savedInstanceState)
bannerView = ViewUtil.findStubById(view, R.id.banner_stub)
updateBanners()
initializeBanners()
}
override fun bindAdapter(adapter: MappingAdapter) {
@ -74,22 +73,12 @@ class AppSettingsFragment : DSLSettingsFragment(
}
}
private fun updateBanners() {
val unauthorizedProducer = UnauthorizedBanner.Producer(requireContext())
lifecycle.addObserver(
SharedPreferencesLifecycleObserver(
requireContext(),
mapOf(
TextSecurePreferences.UNAUTHORIZED_RECEIVED to { unauthorizedProducer.queryAndEmit() }
)
)
)
val bannerFlows = listOf(
OutdatedBuildBanner.createFlow(requireContext(), OutdatedBuildBanner.ExpiryStatus.EXPIRED_ONLY),
unauthorizedProducer.flow
)
val bannerManager = BannerManager(
bannerFlows,
private fun initializeBanners() {
this.bannerManager = BannerManager(
banners = listOf(
DeprecatedBuildBanner(),
UnauthorizedBanner(requireContext())
),
onNewBannerShownListener = {
if (bannerView.resolved()) {
bannerView.get().addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->
@ -102,13 +91,16 @@ class AppSettingsFragment : DSLSettingsFragment(
recyclerView?.clipToPadding = true
}
)
bannerManager.setContent(bannerView.get())
this.bannerManager?.updateContent(bannerView.get())
viewModel.refreshDeprecatedOrUnregistered()
}
override fun onResume() {
super.onResume()
viewModel.refreshExpiredGiftBadge()
this.bannerManager?.updateContent(bannerView.get())
}
private fun getConfiguration(state: AppSettingsState): DSLConfiguration {

View file

@ -16,7 +16,6 @@ import android.view.View
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.compose.ui.platform.ComposeView
import androidx.core.transition.addListener
import kotlinx.coroutines.flow.Flow
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.BannerManager
@ -58,10 +57,10 @@ class ConversationBannerView @JvmOverloads constructor(
orientation = VERTICAL
}
fun collectAndShowBanners(flows: Iterable<Flow<Banner>>) {
fun collectAndShowBanners(flows: List<Banner<*>>) {
val bannerManager = BannerManager(flows)
show(stub = bannerStub) {
bannerManager.setContent(this)
bannerManager.updateContent(this)
}
}

View file

@ -63,6 +63,8 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.ConversationLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
@ -80,6 +82,10 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@ -106,8 +112,6 @@ import org.thoughtcrime.securesms.badges.gifts.OpenableGift
import org.thoughtcrime.securesms.badges.gifts.OpenableGiftItemDecoration
import org.thoughtcrime.securesms.badges.gifts.viewgift.received.ViewReceivedGiftBottomSheet
import org.thoughtcrime.securesms.badges.gifts.viewgift.sent.ViewSentGiftBottomSheet
import org.thoughtcrime.securesms.banner.banners.ServiceOutageBanner
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.components.AnimatingToggle
import org.thoughtcrime.securesms.components.ComposeText
@ -311,7 +315,6 @@ import org.thoughtcrime.securesms.util.MessageConstraintsUtil.isValidEditMessage
import org.thoughtcrime.securesms.util.PlayStoreUtil
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.SaveAttachmentUtil
import org.thoughtcrime.securesms.util.SharedPreferencesLifecycleObserver
import org.thoughtcrime.securesms.util.SignalLocalMetrics
import org.thoughtcrime.securesms.util.StorageUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
@ -1025,33 +1028,26 @@ class ConversationFragment :
val conversationBannerListener = ConversationBannerListener()
binding.conversationBanner.listener = conversationBannerListener
val unauthorizedProducer = UnauthorizedBanner.Producer(requireContext())
val serviceOutageProducer = ServiceOutageBanner.Producer(requireContext())
lifecycle.addObserver(
SharedPreferencesLifecycleObserver(
requireContext(),
mapOf(
TextSecurePreferences.UNAUTHORIZED_RECEIVED to { unauthorizedProducer.queryAndEmit() },
TextSecurePreferences.SERVICE_OUTAGE to { serviceOutageProducer.queryAndEmit() }
lifecycleScope.launch {
viewModel
.getBannerFlows(
context = requireContext(),
groupJoinClickListener = conversationBannerListener::reviewJoinRequestsAction,
onSuggestionAddMembers = {
conversationGroupViewModel.groupRecordSnapshot?.let { groupRecord ->
GroupsV1MigrationSuggestionsDialog.show(requireActivity(), groupRecord.id.requireV2(), groupRecord.gv1MigrationSuggestions)
}
},
onSuggestionNoThanks = conversationGroupViewModel::onSuggestedMembersBannerDismissed,
bubbleClickListener = conversationBannerListener::changeBubbleSettingAction
)
)
)
val bannerFlows = viewModel.getBannerFlows(
context = requireContext(),
unauthorizedFlow = unauthorizedProducer.flow,
serviceOutageStatusFlow = serviceOutageProducer.flow,
groupJoinClickListener = conversationBannerListener::reviewJoinRequestsAction,
onAddMembers = {
conversationGroupViewModel.groupRecordSnapshot?.let { groupRecord ->
GroupsV1MigrationSuggestionsDialog.show(requireActivity(), groupRecord.id.requireV2(), groupRecord.gv1MigrationSuggestions)
.distinctUntilChanged()
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.flowOn(Dispatchers.Main)
.collect {
binding.conversationBanner.collectAndShowBanners(it)
}
},
onNoThanks = conversationGroupViewModel::onSuggestedMembersBannerDismissed,
bubbleClickListener = conversationBannerListener::changeBubbleSettingAction
)
binding.conversationBanner.collectAndShowBanners(bannerFlows)
}
if (TextSecurePreferences.getServiceOutage(context)) {
AppDependencies.jobManager.add(ServiceOutageDetectionJob())

View file

@ -29,12 +29,13 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.rx3.asFlow
import org.signal.core.util.orNull
import org.signal.paging.ProxyPagingController
@ -309,30 +310,42 @@ class ConversationViewModel(
})
}
@OptIn(ExperimentalCoroutinesApi::class)
fun getBannerFlows(context: Context, unauthorizedFlow: Flow<UnauthorizedBanner>, serviceOutageStatusFlow: Flow<ServiceOutageBanner>, groupJoinClickListener: () -> Unit, onAddMembers: () -> Unit, onNoThanks: () -> Unit, bubbleClickListener: (Boolean) -> Unit): List<Flow<Banner>> {
val pendingGroupJoinFlow: Flow<PendingGroupJoinRequestsBanner> = merge(
flow {
emit(PendingGroupJoinRequestsBanner(false, 0, {}, {}))
},
groupRecordFlow.flatMapConcat { PendingGroupJoinRequestsBanner.createFlow(it.actionableRequestingMembersCount, groupJoinClickListener) }
)
fun getBannerFlows(
context: Context,
groupJoinClickListener: () -> Unit,
onSuggestionAddMembers: () -> Unit,
onSuggestionNoThanks: () -> Unit,
bubbleClickListener: (Boolean) -> Unit
): Flow<List<Banner<*>>> {
val pendingGroupJoinFlow: Flow<PendingGroupJoinRequestsBanner> = groupRecordFlow
.map {
PendingGroupJoinRequestsBanner(
suggestionsSize = it.actionableRequestingMembersCount,
onViewClicked = groupJoinClickListener
)
}
val groupV1SuggestionsFlow = merge(
flow {
emit(GroupsV1MigrationSuggestionsBanner(0, {}, {}))
},
groupRecordFlow.flatMapConcat { GroupsV1MigrationSuggestionsBanner.createFlow(it.gv1MigrationSuggestions.size, onAddMembers, onNoThanks) }
)
val groupV1SuggestionsFlow = groupRecordFlow
.map {
GroupsV1MigrationSuggestionsBanner(
suggestionsSize = it.gv1MigrationSuggestions.size,
onAddMembers = onSuggestionAddMembers,
onNoThanks = onSuggestionNoThanks
)
}
return listOf(
OutdatedBuildBanner.createFlow(context, OutdatedBuildBanner.ExpiryStatus.EXPIRED_ONLY),
unauthorizedFlow,
serviceOutageStatusFlow,
pendingGroupJoinFlow,
groupV1SuggestionsFlow,
BubbleOptOutBanner.createFlow(inBubble = repository.isInBubble, bubbleClickListener)
return combine(
listOf(
flowOf(OutdatedBuildBanner()),
flowOf(UnauthorizedBanner(context)),
flowOf(ServiceOutageBanner(context)),
pendingGroupJoinFlow,
groupV1SuggestionsFlow,
flowOf(BubbleOptOutBanner(inBubble = repository.isInBubble, actionListener = bubbleClickListener))
),
transform = { it.toList() }
)
.flowOn(Dispatchers.IO)
}
fun onChatBoundsChanged(bounds: Rect) {

View file

@ -98,6 +98,7 @@ import org.thoughtcrime.securesms.banner.Banner;
import org.thoughtcrime.securesms.banner.BannerManager;
import org.thoughtcrime.securesms.banner.banners.CdsPermanentErrorBanner;
import org.thoughtcrime.securesms.banner.banners.CdsTemporaryErrorBanner;
import org.thoughtcrime.securesms.banner.banners.DeprecatedBuildBanner;
import org.thoughtcrime.securesms.banner.banners.DozeBanner;
import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner;
import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner;
@ -138,6 +139,7 @@ import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
import org.thoughtcrime.securesms.keyvalue.AccountValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity;
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder;
@ -166,7 +168,6 @@ import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SharedPreferencesLifecycleObserver;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalProxyUtil;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
@ -184,18 +185,14 @@ import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
import kotlinx.coroutines.flow.Flow;
import static android.app.Activity.RESULT_OK;
@ -246,6 +243,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private ConversationListTabsViewModel conversationListTabsViewModel;
private ContactSearchMediator contactSearchMediator;
private BannerManager bannerManager;
public static ConversationListFragment newInstance() {
return new ConversationListFragment();
}
@ -524,6 +523,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
getParentFragmentManager()
);
}
if (this.bannerManager != null) {
this.bannerManager.updateContent(bannerView.get());
}
}
@Override
@ -849,41 +852,27 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}
private void initializeBanners() {
Map<String, Function0<Unit>> listenerMap = new HashMap<>();
final UnauthorizedBanner.Producer unauthorizedBannerProducer = new UnauthorizedBanner.Producer(requireContext());
final ServiceOutageBanner.Producer serviceOutageBannerProducer = new ServiceOutageBanner.Producer(requireContext());
List<Banner<?>> bannerRepositories = List.of(
new DeprecatedBuildBanner(),
new UnauthorizedBanner(requireContext()),
new ServiceOutageBanner(requireContext()),
new OutdatedBuildBanner(),
new DozeBanner(requireContext()),
new CdsTemporaryErrorBanner(getChildFragmentManager()),
new CdsPermanentErrorBanner(getChildFragmentManager()),
new UsernameOutOfSyncBanner((usernameSyncState) -> {
if (usernameSyncState == AccountValues.UsernameSyncState.USERNAME_AND_LINK_CORRUPTED) {
startActivityForResult(AppSettingsActivity.usernameRecovery(requireContext()), UsernameEditFragment.REQUEST_CODE);
} else {
startActivity(AppSettingsActivity.usernameLinkSettings(requireContext()));
}
return Unit.INSTANCE;
}),
new MediaRestoreProgressBanner()
);
listenerMap.put(TextSecurePreferences.UNAUTHORIZED_RECEIVED, () -> {
unauthorizedBannerProducer.queryAndEmit();
return Unit.INSTANCE;
});
listenerMap.put(TextSecurePreferences.SERVICE_OUTAGE, () -> {
serviceOutageBannerProducer.queryAndEmit();
return Unit.INSTANCE;
});
final SharedPreferencesLifecycleObserver sharedPrefsObserver = new SharedPreferencesLifecycleObserver(requireContext(), listenerMap);
getLifecycle().addObserver(sharedPrefsObserver);
final List<Flow<? extends Banner>> bannerRepositories = List.of(OutdatedBuildBanner.createFlow(requireContext(), OutdatedBuildBanner.ExpiryStatus.EXPIRED_ONLY),
unauthorizedBannerProducer.getFlow(),
serviceOutageBannerProducer.getFlow(),
OutdatedBuildBanner.createFlow(requireContext(), OutdatedBuildBanner.ExpiryStatus.OUTDATED_ONLY),
DozeBanner.createFlow(requireContext()),
CdsTemporaryErrorBanner.createFlow(getChildFragmentManager()),
CdsPermanentErrorBanner.createFlow(getChildFragmentManager()),
UsernameOutOfSyncBanner.createFlow(requireContext(), usernameCorruptedToo -> {
if (usernameCorruptedToo) {
startActivityForResult(AppSettingsActivity.usernameRecovery(requireContext()), UsernameEditFragment.REQUEST_CODE);
} else {
startActivity(AppSettingsActivity.usernameLinkSettings(requireContext()));
}
return Unit.INSTANCE;
}),
MediaRestoreProgressBanner.createLifecycleAwareFlow(getViewLifecycleOwner()));
final BannerManager bannerManager = new BannerManager(bannerRepositories);
bannerManager.setContent(bannerView.get());
this.bannerManager = new BannerManager(bannerRepositories);
this.bannerManager.updateContent(bannerView.get());
}
private void maybeScheduleRefreshProfileJob() {

View file

@ -40,15 +40,14 @@ import org.thoughtcrime.securesms.payments.backup.RecoveryPhraseStates;
import org.thoughtcrime.securesms.payments.backup.confirm.PaymentsRecoveryPhraseConfirmFragment;
import org.thoughtcrime.securesms.payments.preferences.model.InfoCard;
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.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -265,11 +264,11 @@ public class PaymentsHomeFragment extends LoggingFragment {
if (failure) {
showUpdateIsRequiredDialog();
}
BannerManager bannerManager = new BannerManager(List.of(new EnclaveFailureBanner(failure)));
bannerManager.updateContent(bannerView.get());
});
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());
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressed());
}

View file

@ -27,7 +27,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.BannerManager
import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner
import org.thoughtcrime.securesms.banner.banners.DeprecatedBuildBanner
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner
import org.thoughtcrime.securesms.components.Material3SearchToolbar
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
@ -55,8 +55,6 @@ import org.thoughtcrime.securesms.stories.settings.StorySettingsActivity
import org.thoughtcrime.securesms.stories.tabs.ConversationListTab
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel
import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity
import org.thoughtcrime.securesms.util.SharedPreferencesLifecycleObserver
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.fragments.requireListener
@ -141,21 +139,11 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
}
private fun initializeBanners() {
val unauthorizedProducer = UnauthorizedBanner.Producer(requireContext())
lifecycle.addObserver(
SharedPreferencesLifecycleObserver(
requireContext(),
mapOf(
TextSecurePreferences.UNAUTHORIZED_RECEIVED to { unauthorizedProducer.queryAndEmit() }
)
)
)
val bannerFlows = listOf(
OutdatedBuildBanner.createFlow(requireContext(), OutdatedBuildBanner.ExpiryStatus.EXPIRED_ONLY),
unauthorizedProducer.flow
)
val bannerManager = BannerManager(
bannerFlows,
banners = listOf(
DeprecatedBuildBanner(),
UnauthorizedBanner(requireContext())
),
onNewBannerShownListener = {
if (bannerView.resolved()) {
bannerView.get().addOnLayoutChangeListener { _, _, top, _, bottom, _, _, _, _ ->
@ -168,7 +156,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
recyclerView?.clipToPadding = true
}
)
bannerManager.setContent(bannerView.get())
bannerManager.updateContent(bannerView.get())
}
override fun bindAdapter(adapter: MappingAdapter) {