Fix dismissible banners.

This commit is contained in:
Nicholas Tinsley 2024-08-15 12:00:21 -04:00 committed by mtang-signal
parent 630875dae2
commit 560086a1c2
8 changed files with 100 additions and 47 deletions

View file

@ -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 <T : Banner> createAndEmit(bannerFactory: () -> T?): Flow<T> {
return bannerFactory()?.let {
fun <T : Banner> createAndEmit(bannerFactory: () -> T): Flow<T> {
return bannerFactory().let {
flow { emit(it) }
} ?: emptyFlow()
}
}
}

View file

@ -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<T : Banner>(bannerProducer: (dismissListener: () -> Unit) -> T) {
abstract fun createDismissedBanner(): T
private val mutableSharedFlow: MutableSharedFlow<T> = MutableSharedFlow(replay = 1)
private val dismissListener = {
mutableSharedFlow.tryEmit(createDismissedBanner())
}
init {
mutableSharedFlow.tryEmit(bannerProducer(dismissListener))
}
val flow: Flow<T> = mutableSharedFlow
}

View file

@ -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<BubbleOptOutBanner>(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<BubbleOptOutBanner> = createAndEmit {
BubbleOptOutBanner(inBubble, actionListener)
fun createFlow(inBubble: Boolean, actionListener: (Boolean) -> Unit): Flow<BubbleOptOutBanner> {
return Producer(inBubble, actionListener).flow
}
}
}

View file

@ -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<DozeBanner>(bannerProducer = {
DozeBanner(context = context, dismissed = false, onDismiss = it)
}) {
override fun createDismissedBanner(): DozeBanner {
return DozeBanner(context, true) {}
}
}
companion object {
@JvmStatic
fun createFlow(context: Context): Flow<DozeBanner> = createAndEmit {
DozeBanner(context)
fun createFlow(context: Context): Flow<DozeBanner> {
return Producer(context).flow
}
}
}

View file

@ -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<GroupsV1MigrationSuggestionsBanner>(bannerProducer = {
GroupsV1MigrationSuggestionsBanner(
suggestionsSize,
onAddMembers
) {
onNoThanks()
it()
}
}) {
override fun createDismissedBanner(): GroupsV1MigrationSuggestionsBanner {
return GroupsV1MigrationSuggestionsBanner(0, {}, {})
}
}
@JvmStatic
fun createFlow(suggestionsSize: Int, onAddMembers: () -> Unit, onNoThanks: () -> Unit): Flow<GroupsV1MigrationSuggestionsBanner> = createAndEmit {
GroupsV1MigrationSuggestionsBanner(suggestionsSize, onAddMembers, onNoThanks)
companion object {
fun createFlow(suggestionsSize: Int, onAddMembers: () -> Unit, onNoThanks: () -> Unit): Flow<GroupsV1MigrationSuggestionsBanner> {
return Producer(suggestionsSize, onAddMembers, onNoThanks).flow
}
}
}

View file

@ -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<PendingGroupJoinRequestsBanner>(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<PendingGroupJoinRequestsBanner> {
return Producer(suggestionsSize, onViewClicked).flow
}
private val mutableStateFlow: MutableStateFlow<PendingGroupJoinRequestsBanner> = MutableStateFlow(PendingGroupJoinRequestsBanner(suggestionsSize > 0, suggestionsSize, onViewClicked, dismissListener))
val flow: Flow<PendingGroupJoinRequestsBanner> = mutableStateFlow
}
}

View file

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

View file

@ -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<Unit> = PublishSubject.create()
val reminder: Observable<Optional<Reminder>>
private val groupRecordFlow: Flow<GroupRecord?>
private val groupRecordFlow: Flow<GroupRecord>
private val refreshIdentityRecords: Subject<Unit> = PublishSubject.create()
private val identityRecordsStore: RxStore<IdentityRecordsState> = 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<Flow<Banner>> {
val pendingGroupJoinFlow = groupRecordFlow.flatMapConcat {
val pendingGroupJoinFlow: Flow<PendingGroupJoinRequestsBanner> = 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)
)
}