From 560086a1c29aab614db17b84445dca9f54582928 Mon Sep 17 00:00:00 2001 From: Nicholas Tinsley Date: Thu, 15 Aug 2024 12:00:21 -0400 Subject: [PATCH] Fix dismissible banners. --- .../thoughtcrime/securesms/banner/Banner.kt | 7 ++-- .../banner/DismissibleBannerProducer.kt | 24 +++++++++++++ .../banner/banners/BubbleOptOutBanner.kt | 17 +++++++-- .../securesms/banner/banners/DozeBanner.kt | 19 +++++++--- .../GroupsV1MigrationSuggestionsBanner.kt | 24 +++++++++---- .../banners/PendingGroupJoinRequestsBanner.kt | 18 ++++++---- .../conversation/v2/ConversationRepository.kt | 2 +- .../conversation/v2/ConversationViewModel.kt | 36 ++++++++----------- 8 files changed, 100 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/banner/DismissibleBannerProducer.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/Banner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/Banner.kt index aa658db26e..bb3ee57197 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/Banner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/Banner.kt @@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.banner import androidx.compose.runtime.Composable import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flow import org.signal.core.util.logging.Log @@ -27,10 +26,10 @@ abstract class Banner { * @param bannerFactory a block the produces a [Banner], or null. Returning null will complete the [Flow] without emitting any values. */ @JvmStatic - fun createAndEmit(bannerFactory: () -> T?): Flow { - return bannerFactory()?.let { + fun createAndEmit(bannerFactory: () -> T): Flow { + return bannerFactory().let { flow { emit(it) } - } ?: emptyFlow() + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/DismissibleBannerProducer.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/DismissibleBannerProducer.kt new file mode 100644 index 0000000000..e0acb4ef1a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/DismissibleBannerProducer.kt @@ -0,0 +1,24 @@ +/* + * 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(bannerProducer: (dismissListener: () -> Unit) -> T) { + abstract fun createDismissedBanner(): T + + private val mutableSharedFlow: MutableSharedFlow = MutableSharedFlow(replay = 1) + private val dismissListener = { + mutableSharedFlow.tryEmit(createDismissedBanner()) + } + + init { + mutableSharedFlow.tryEmit(bannerProducer(dismissListener)) + } + + val flow: Flow = mutableSharedFlow +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/BubbleOptOutBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/BubbleOptOutBanner.kt index 8ae1b70012..6d71bd0e6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/BubbleOptOutBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/BubbleOptOutBanner.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.res.stringResource import kotlinx.coroutines.flow.Flow import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.DismissibleBannerProducer import org.thoughtcrime.securesms.banner.ui.compose.Action import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -35,10 +36,20 @@ class BubbleOptOutBanner(inBubble: Boolean, private val actionListener: (Boolean ) } + private class Producer(inBubble: Boolean, actionListener: (Boolean) -> Unit) : DismissibleBannerProducer(bannerProducer = { + BubbleOptOutBanner(inBubble) { turnOffBubbles -> + actionListener(turnOffBubbles) + it() + } + }) { + override fun createDismissedBanner(): BubbleOptOutBanner { + return BubbleOptOutBanner(false) {} + } + } + companion object { - @JvmStatic - fun createFlow(inBubble: Boolean, actionListener: (Boolean) -> Unit): Flow = createAndEmit { - BubbleOptOutBanner(inBubble, actionListener) + fun createFlow(inBubble: Boolean, actionListener: (Boolean) -> Unit): Flow { + return Producer(inBubble, actionListener).flow } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/DozeBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/DozeBanner.kt index 1a225bdd07..ff850786c4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/DozeBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/DozeBanner.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.res.stringResource import kotlinx.coroutines.flow.Flow import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.DismissibleBannerProducer import org.thoughtcrime.securesms.banner.ui.compose.Action import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -19,8 +20,8 @@ import org.thoughtcrime.securesms.util.PowerManagerCompat import org.thoughtcrime.securesms.util.ServiceUtil import org.thoughtcrime.securesms.util.TextSecurePreferences -class DozeBanner(private val context: Context) : Banner() { - override val enabled: Boolean = +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) @Composable @@ -39,15 +40,23 @@ class DozeBanner(private val context: Context) : Banner() { ), onDismissListener = { TextSecurePreferences.setPromptedOptimizeDoze(context, true) + onDismiss() } ) } - companion object { + private class Producer(private val context: Context) : DismissibleBannerProducer(bannerProducer = { + DozeBanner(context = context, dismissed = false, onDismiss = it) + }) { + override fun createDismissedBanner(): DozeBanner { + return DozeBanner(context, true) {} + } + } + companion object { @JvmStatic - fun createFlow(context: Context): Flow = createAndEmit { - DozeBanner(context) + fun createFlow(context: Context): Flow { + return Producer(context).flow } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/GroupsV1MigrationSuggestionsBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/GroupsV1MigrationSuggestionsBanner.kt index ee4bb5c965..969ad8d996 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/GroupsV1MigrationSuggestionsBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/GroupsV1MigrationSuggestionsBanner.kt @@ -10,13 +10,11 @@ import androidx.compose.ui.res.pluralStringResource import kotlinx.coroutines.flow.Flow import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.banner.Banner +import org.thoughtcrime.securesms.banner.DismissibleBannerProducer import org.thoughtcrime.securesms.banner.ui.compose.Action import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner -import org.thoughtcrime.securesms.keyvalue.SignalStore class GroupsV1MigrationSuggestionsBanner(private val suggestionsSize: Int, private val onAddMembers: () -> Unit, private val onNoThanks: () -> Unit) : Banner() { - private val timeUntilUnblock = SignalStore.misc.cdsBlockedUtil - System.currentTimeMillis() - override val enabled: Boolean = suggestionsSize > 0 @Composable @@ -35,11 +33,23 @@ class GroupsV1MigrationSuggestionsBanner(private val suggestionsSize: Int, priva ) } - companion object { + private class Producer(suggestionsSize: Int, onAddMembers: () -> Unit, onNoThanks: () -> Unit) : DismissibleBannerProducer(bannerProducer = { + GroupsV1MigrationSuggestionsBanner( + suggestionsSize, + onAddMembers + ) { + onNoThanks() + it() + } + }) { + override fun createDismissedBanner(): GroupsV1MigrationSuggestionsBanner { + return GroupsV1MigrationSuggestionsBanner(0, {}, {}) + } + } - @JvmStatic - fun createFlow(suggestionsSize: Int, onAddMembers: () -> Unit, onNoThanks: () -> Unit): Flow = createAndEmit { - GroupsV1MigrationSuggestionsBanner(suggestionsSize, onAddMembers, onNoThanks) + companion object { + fun createFlow(suggestionsSize: Int, onAddMembers: () -> Unit, onNoThanks: () -> Unit): Flow { + return Producer(suggestionsSize, onAddMembers, onNoThanks).flow } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/PendingGroupJoinRequestsBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/PendingGroupJoinRequestsBanner.kt index bc962eac50..7782e0f84d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/PendingGroupJoinRequestsBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/PendingGroupJoinRequestsBanner.kt @@ -8,9 +8,9 @@ package org.thoughtcrime.securesms.banner.banners import androidx.compose.runtime.Composable import androidx.compose.ui.res.pluralStringResource import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import 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 @@ -32,11 +32,17 @@ class PendingGroupJoinRequestsBanner(override val enabled: Boolean, private val ) } - class Producer(suggestionsSize: Int, onViewClicked: () -> Unit) { - private val dismissListener: () -> Unit = { - mutableStateFlow.tryEmit(PendingGroupJoinRequestsBanner(false, suggestionsSize, onViewClicked, null)) + private class Producer(suggestionsSize: Int, onViewClicked: () -> Unit) : DismissibleBannerProducer(bannerProducer = { + PendingGroupJoinRequestsBanner(suggestionsSize > 0, suggestionsSize, onViewClicked, it) + }) { + override fun createDismissedBanner(): PendingGroupJoinRequestsBanner { + return PendingGroupJoinRequestsBanner(false, 0, {}, null) + } + } + + companion object { + fun createFlow(suggestionsSize: Int, onViewClicked: () -> Unit): Flow { + return Producer(suggestionsSize, onViewClicked).flow } - private val mutableStateFlow: MutableStateFlow = MutableStateFlow(PendingGroupJoinRequestsBanner(suggestionsSize > 0, suggestionsSize, onViewClicked, dismissListener)) - val flow: Flow = mutableStateFlow } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt index e8a8f2bd68..62b2e42847 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationRepository.kt @@ -108,7 +108,7 @@ import kotlin.time.Duration.Companion.seconds class ConversationRepository( private val localContext: Context, - private val isInBubble: Boolean + val isInBubble: Boolean ) { companion object { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index e3a809fc16..780b4fd27d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -31,11 +31,10 @@ import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.Subject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flatMap import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flow -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.concurrent.subscribeWithSubject import org.signal.core.util.orNull @@ -45,7 +44,6 @@ import org.thoughtcrime.securesms.banner.banners.BubbleOptOutBanner import org.thoughtcrime.securesms.banner.banners.GroupsV1MigrationSuggestionsBanner import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner import org.thoughtcrime.securesms.banner.banners.PendingGroupJoinRequestsBanner -import org.thoughtcrime.securesms.banner.banners.PendingGroupJoinRequestsBanner.Producer import org.thoughtcrime.securesms.banner.banners.ServiceOutageBanner import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner import org.thoughtcrime.securesms.components.reminder.Reminder @@ -172,7 +170,7 @@ class ConversationViewModel( private val refreshReminder: Subject = PublishSubject.create() val reminder: Observable> - private val groupRecordFlow: Flow + private val groupRecordFlow: Flow private val refreshIdentityRecords: Subject = PublishSubject.create() private val identityRecordsStore: RxStore = RxStore(IdentityRecordsState()) @@ -295,7 +293,7 @@ class ConversationViewModel( .flatMapMaybe { groupRecord -> repository.getReminder(groupRecord.orNull()) } .observeOn(AndroidSchedulers.mainThread()) - groupRecordFlow = recipientRepository.groupRecord.subscribeOn(Schedulers.io()).asFlow().map { it.orNull() } + groupRecordFlow = recipientRepository.groupRecord.subscribeOn(Schedulers.io()).asFlow().mapNotNull { it.orNull() } Observable.combineLatest( refreshIdentityRecords.startWithItem(Unit).observeOn(Schedulers.io()), @@ -322,23 +320,19 @@ class ConversationViewModel( @OptIn(ExperimentalCoroutinesApi::class) fun getBannerFlows(context: Context, groupJoinClickListener: () -> Unit, onAddMembers: () -> Unit, onNoThanks: () -> Unit, bubbleClickListener: (Boolean) -> Unit): List> { - val pendingGroupJoinFlow = groupRecordFlow.flatMapConcat { + val pendingGroupJoinFlow: Flow = merge( flow { - if (it == null) { - emit(PendingGroupJoinRequestsBanner(false, 0, {}, {})) - } else { - emitAll(Producer(it.actionableRequestingMembersCount, groupJoinClickListener).flow) - } - } - } + emit(PendingGroupJoinRequestsBanner(false, 0, {}, {})) + }, + groupRecordFlow.flatMapConcat { PendingGroupJoinRequestsBanner.createFlow(it.actionableRequestingMembersCount, groupJoinClickListener) } + ) - val groupV1SuggestionsFlow = groupRecordFlow.map { - if (it == null) { + val groupV1SuggestionsFlow = merge( + flow { GroupsV1MigrationSuggestionsBanner(0, {}, {}) - } else { - GroupsV1MigrationSuggestionsBanner(it.gv1MigrationSuggestions.size, onAddMembers, onNoThanks) - } - } + }, + groupRecordFlow.flatMapConcat { GroupsV1MigrationSuggestionsBanner.createFlow(it.gv1MigrationSuggestions.size, onAddMembers, onNoThanks) } + ) return listOf( OutdatedBuildBanner.createFlow(context, OutdatedBuildBanner.ExpiryStatus.EXPIRED_ONLY), @@ -346,7 +340,7 @@ class ConversationViewModel( ServiceOutageBanner.createFlow(context), pendingGroupJoinFlow, groupV1SuggestionsFlow, - BubbleOptOutBanner.createFlow(inBubble = true, bubbleClickListener) + BubbleOptOutBanner.createFlow(inBubble = repository.isInBubble, bubbleClickListener) ) }