Improve the Banner system.
This commit is contained in:
parent
24133c6dac
commit
ba06efe35a
24 changed files with 769 additions and 606 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Reference in a new issue