Add Banners to all reminder usages behind remote config.

This commit is contained in:
Nicholas Tinsley
2024-08-09 17:41:02 -04:00
committed by mtang-signal
parent f296fcd716
commit e2e6a73e8d
18 changed files with 253 additions and 62 deletions

View File

@@ -46,8 +46,8 @@ class CdsPermanentErrorBanner(private val fragmentManager: FragmentManager) : Ba
val PERMANENT_TIME_CUTOFF = 30.days.inWholeMilliseconds val PERMANENT_TIME_CUTOFF = 30.days.inWholeMilliseconds
@JvmStatic @JvmStatic
fun createFlow(fragmentManager: FragmentManager): Flow<CdsPermanentErrorBanner> = createAndEmit { fun createFlow(childFragmentManager: FragmentManager): Flow<CdsPermanentErrorBanner> = createAndEmit {
CdsPermanentErrorBanner(fragmentManager) CdsPermanentErrorBanner(childFragmentManager)
} }
} }
} }

View File

@@ -39,8 +39,8 @@ class CdsTemporaryErrorBanner(private val fragmentManager: FragmentManager) : Ba
companion object { companion object {
@JvmStatic @JvmStatic
fun createFlow(fragmentManager: FragmentManager): Flow<CdsTemporaryErrorBanner> = createAndEmit { fun createFlow(childFragmentManager: FragmentManager): Flow<CdsTemporaryErrorBanner> = createAndEmit {
CdsTemporaryErrorBanner(fragmentManager) CdsTemporaryErrorBanner(childFragmentManager)
} }
} }
} }

View File

@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.banner.banners
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -20,12 +19,15 @@ import org.thoughtcrime.securesms.util.PowerManagerCompat
import org.thoughtcrime.securesms.util.ServiceUtil import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
@RequiresApi(23)
class DozeBanner(private val context: Context) : Banner() { class DozeBanner(private val context: Context) : Banner() {
override val enabled: Boolean = !SignalStore.account.fcmEnabled && !TextSecurePreferences.hasPromptedOptimizeDoze(context) && Build.VERSION.SDK_INT >= 23 && !ServiceUtil.getPowerManager(context).isIgnoringBatteryOptimizations(context.packageName) override val enabled: Boolean =
Build.VERSION.SDK_INT >= 23 && !SignalStore.account.fcmEnabled && !TextSecurePreferences.hasPromptedOptimizeDoze(context) && !ServiceUtil.getPowerManager(context).isIgnoringBatteryOptimizations(context.packageName)
@Composable @Composable
override fun DisplayBanner() { override fun DisplayBanner() {
if (Build.VERSION.SDK_INT < 23) {
throw IllegalStateException("Showing a Doze banner for an OS prior to Android 6.0")
}
DefaultBanner( DefaultBanner(
title = stringResource(id = R.string.DozeReminder_optimize_for_missing_play_services), 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), body = stringResource(id = R.string.DozeReminder_this_device_does_not_support_play_services_tap_to_disable_system_battery),
@@ -45,11 +47,7 @@ class DozeBanner(private val context: Context) : Banner() {
@JvmStatic @JvmStatic
fun createFlow(context: Context): Flow<DozeBanner> = createAndEmit { fun createFlow(context: Context): Flow<DozeBanner> = createAndEmit {
if (Build.VERSION.SDK_INT >= 23) {
DozeBanner(context) DozeBanner(context)
} else {
null
}
} }
} }
} }

View File

@@ -12,7 +12,7 @@ import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -40,7 +40,9 @@ class MediaRestoreProgressBanner(private val data: MediaRestoreEvent) : Banner()
lifecycleOwner.lifecycle.addObserver(observer) lifecycleOwner.lifecycle.addObserver(observer)
return observer.flow return observer.flow
} else { } else {
return emptyFlow() return flow {
emit(MediaRestoreProgressBanner(MediaRestoreEvent(0L, 0L)))
}
} }
} }
} }

View File

@@ -22,22 +22,38 @@ import kotlin.time.Duration.Companion.milliseconds
/** /**
* Banner to let the user know their build is about to expire or has expired. * 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.
*/ */
class OutdatedBuildBanner(val context: Context, private val daysUntilExpiry: Int) : Banner() { class OutdatedBuildBanner(val context: Context, private val daysUntilExpiry: Int, private val status: ExpiryStatus) : Banner() {
override val enabled = SignalStore.misc.isClientDeprecated || daysUntilExpiry <= MAX_DAYS_UNTIL_EXPIRE 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 @Composable
override fun DisplayBanner() { override fun DisplayBanner() {
DefaultBanner( val bodyText = when (status) {
title = null, ExpiryStatus.OUTDATED_ONLY -> if (daysUntilExpiry == 0) {
body = if (SignalStore.misc.isClientDeprecated) { 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) stringResource(id = R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today)
} else if (daysUntilExpiry == 0) { } else if (daysUntilExpiry == 0) {
stringResource(id = R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today) stringResource(id = R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today)
} else { } else {
pluralStringResource(id = R.plurals.OutdatedBuildReminder_your_version_of_signal_will_expire_in_n_days, count = daysUntilExpiry, daysUntilExpiry) 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 = if (SignalStore.misc.isClientDeprecated) {
Importance.ERROR Importance.ERROR
} else { } else {
@@ -51,13 +67,25 @@ class OutdatedBuildBanner(val context: Context, private val daysUntilExpiry: Int
) )
} }
/**
* 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
}
companion object { companion object {
private const val MAX_DAYS_UNTIL_EXPIRE = 10 private const val MAX_DAYS_UNTIL_EXPIRE = 10
@JvmStatic @JvmStatic
fun createFlow(context: Context): Flow<OutdatedBuildBanner> = createAndEmit { fun createFlow(context: Context, status: ExpiryStatus): Flow<OutdatedBuildBanner> = createAndEmit {
val daysUntilExpiry = Util.getTimeUntilBuildExpiry(SignalStore.misc.estimatedServerTime).milliseconds.inWholeDays.toInt() val daysUntilExpiry = Util.getTimeUntilBuildExpiry(SignalStore.misc.estimatedServerTime).milliseconds.inWholeDays.toInt()
OutdatedBuildBanner(context, daysUntilExpiry) OutdatedBuildBanner(context, daysUntilExpiry, status)
} }
} }
} }

View File

@@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flow
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -33,14 +32,8 @@ class PendingGroupJoinRequestsBanner(override val enabled: Boolean, private val
) )
} }
companion object { class Producer(suggestionsSize: Int, onViewClicked: () -> Unit) {
private val dismissListener: () -> Unit = {
@JvmStatic
fun createFlow(suggestionsSize: Int, onViewClicked: () -> Unit): Flow<PendingGroupJoinRequestsBanner> = Producer(suggestionsSize, onViewClicked).flow
}
private class Producer(suggestionsSize: Int, onViewClicked: () -> Unit) {
val dismissListener: () -> Unit = {
mutableStateFlow.tryEmit(PendingGroupJoinRequestsBanner(false, suggestionsSize, onViewClicked, null)) mutableStateFlow.tryEmit(PendingGroupJoinRequestsBanner(false, suggestionsSize, onViewClicked, null))
} }
private val mutableStateFlow: MutableStateFlow<PendingGroupJoinRequestsBanner> = MutableStateFlow(PendingGroupJoinRequestsBanner(true, suggestionsSize, onViewClicked, dismissListener)) private val mutableStateFlow: MutableStateFlow<PendingGroupJoinRequestsBanner> = MutableStateFlow(PendingGroupJoinRequestsBanner(true, suggestionsSize, onViewClicked, dismissListener))

View File

@@ -46,6 +46,9 @@ class UsernameOutOfSyncBanner(private val context: Context, private val username
companion object { companion object {
/**
* @param onActionClick input is true if both the username and the link are corrupted, false if only the link is corrupted
*/
@JvmStatic @JvmStatic
fun createFlow(context: Context, onActionClick: (Boolean) -> Unit): Flow<UsernameOutOfSyncBanner> = createAndEmit { fun createFlow(context: Context, onActionClick: (Boolean) -> Unit): Flow<UsernameOutOfSyncBanner> = createAndEmit {
UsernameOutOfSyncBanner(context, SignalStore.account.usernameSyncState, onActionClick) UsernameOutOfSyncBanner(context, SignalStore.account.usernameSyncState, onActionClick)

View File

@@ -6,6 +6,7 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
@@ -18,6 +19,9 @@ import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.isNotNullOrBlank import org.signal.core.util.isNotNullOrBlank
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView 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.UnauthorizedBanner
import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.emoji.EmojiTextView import org.thoughtcrime.securesms.components.emoji.EmojiTextView
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder
@@ -59,12 +63,14 @@ class AppSettingsFragment : DSLSettingsFragment(
private val viewModel: AppSettingsViewModel by viewModels() private val viewModel: AppSettingsViewModel by viewModels()
private lateinit var reminderView: Stub<ReminderView> private lateinit var reminderView: Stub<ReminderView>
private lateinit var bannerView: Stub<ComposeView>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycle.addObserver(InAppPaymentsBottomSheetDelegate(childFragmentManager, viewLifecycleOwner)) viewLifecycleOwner.lifecycle.addObserver(InAppPaymentsBottomSheetDelegate(childFragmentManager, viewLifecycleOwner))
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
reminderView = ViewUtil.findStubById(view, R.id.reminder_stub) reminderView = ViewUtil.findStubById(view, R.id.reminder_stub)
bannerView = ViewUtil.findStubById(view, R.id.banner_stub)
updateReminders() updateReminders()
} }
@@ -85,6 +91,14 @@ class AppSettingsFragment : DSLSettingsFragment(
} }
private fun updateReminders() { private fun updateReminders() {
if (RemoteConfig.newBannerUi) {
val bannerFlows = listOf(
OutdatedBuildBanner.createFlow(requireContext(), OutdatedBuildBanner.ExpiryStatus.EXPIRED_ONLY),
UnauthorizedBanner.createFlow(requireContext())
)
val bannerManager = BannerManager(bannerFlows)
bannerManager.setContent(bannerView.get())
} else {
if (ExpiredBuildReminder.isEligible()) { if (ExpiredBuildReminder.isEligible()) {
showReminder(ExpiredBuildReminder(context)) showReminder(ExpiredBuildReminder(context))
} else if (UnauthorizedReminder.isEligible(context)) { } else if (UnauthorizedReminder.isEligible(context)) {
@@ -92,6 +106,7 @@ class AppSettingsFragment : DSLSettingsFragment(
} else { } else {
hideReminders() hideReminders()
} }
}
viewModel.refreshDeprecatedOrUnregistered() viewModel.refreshDeprecatedOrUnregistered()
} }

View File

@@ -14,8 +14,12 @@ import android.util.AttributeSet
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import androidx.appcompat.widget.LinearLayoutCompat import androidx.appcompat.widget.LinearLayoutCompat
import androidx.compose.ui.platform.ComposeView
import androidx.core.transition.addListener import androidx.core.transition.addListener
import kotlinx.coroutines.flow.Flow
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.BannerManager
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView
import org.thoughtcrime.securesms.components.reminder.Reminder import org.thoughtcrime.securesms.components.reminder.Reminder
import org.thoughtcrime.securesms.components.reminder.ReminderView import org.thoughtcrime.securesms.components.reminder.ReminderView
@@ -47,6 +51,7 @@ class ConversationBannerView @JvmOverloads constructor(
) : LinearLayoutCompat(context, attrs, defStyleAttr) { ) : LinearLayoutCompat(context, attrs, defStyleAttr) {
private val unverifiedBannerStub: Stub<UnverifiedBannerView> by lazy { ViewUtil.findStubById(this, R.id.unverified_banner_stub) } private val unverifiedBannerStub: Stub<UnverifiedBannerView> by lazy { ViewUtil.findStubById(this, R.id.unverified_banner_stub) }
private val reminderStub: Stub<ReminderView> by lazy { ViewUtil.findStubById(this, R.id.reminder_stub) } private val reminderStub: Stub<ReminderView> by lazy { ViewUtil.findStubById(this, R.id.reminder_stub) }
private val bannerStub: Stub<ComposeView> by lazy { ViewUtil.findStubById(this, R.id.banner_stub) }
private val reviewBannerStub: Stub<ReviewBannerView> by lazy { ViewUtil.findStubById(this, R.id.review_banner_stub) } private val reviewBannerStub: Stub<ReviewBannerView> by lazy { ViewUtil.findStubById(this, R.id.review_banner_stub) }
private val voiceNotePlayerStub: Stub<View> by lazy { ViewUtil.findStubById(this, R.id.voice_note_player_stub) } private val voiceNotePlayerStub: Stub<View> by lazy { ViewUtil.findStubById(this, R.id.voice_note_player_stub) }
@@ -56,6 +61,13 @@ class ConversationBannerView @JvmOverloads constructor(
orientation = VERTICAL orientation = VERTICAL
} }
fun collectAndShowBanners(flows: Iterable<Flow<Banner>>) {
val bannerManager = BannerManager(flows)
show(stub = bannerStub) {
bannerManager.setContent(this)
}
}
fun showReminder(reminder: Reminder) { fun showReminder(reminder: Reminder) {
show( show(
stub = reminderStub stub = reminderStub

View File

@@ -228,6 +228,7 @@ import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBotto
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestionsDialog import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationSuggestionsDialog
import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult
import org.thoughtcrime.securesms.invites.InviteActions import org.thoughtcrime.securesms.invites.InviteActions
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob
import org.thoughtcrime.securesms.keyboard.KeyboardPage import org.thoughtcrime.securesms.keyboard.KeyboardPage
import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment
import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel
@@ -1016,7 +1017,27 @@ class ConversationFragment :
VoiceMessageRecordingSessionCallbacks() VoiceMessageRecordingSessionCallbacks()
) )
binding.conversationBanner.listener = ConversationBannerListener() val conversationBannerListener = ConversationBannerListener()
binding.conversationBanner.listener = conversationBannerListener
if (RemoteConfig.newBannerUi) {
val bannerFlows = viewModel.getBannerFlows(
context = requireContext(),
groupJoinClickListener = conversationBannerListener::reviewJoinRequestsAction,
onAddMembers = {
conversationGroupViewModel.groupRecordSnapshot?.let { groupRecord ->
GroupsV1MigrationSuggestionsDialog.show(requireActivity(), groupRecord.id.requireV2(), groupRecord.gv1MigrationSuggestions)
}
},
onNoThanks = conversationGroupViewModel::onSuggestedMembersBannerDismissed,
bubbleClickListener = conversationBannerListener::changeBubbleSettingAction
)
binding.conversationBanner.collectAndShowBanners(bannerFlows)
if (TextSecurePreferences.getServiceOutage(context)) {
AppDependencies.jobManager.add(ServiceOutageDetectionJob())
}
} else {
viewModel viewModel
.reminder .reminder
.subscribeBy { reminder -> .subscribeBy { reminder ->
@@ -1027,6 +1048,7 @@ class ConversationFragment :
} }
} }
.addTo(disposables) .addTo(disposables)
}
viewModel viewModel
.identityRecordsObservable .identityRecordsObservable

View File

@@ -29,9 +29,25 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.BehaviorSubject import io.reactivex.rxjava3.subjects.BehaviorSubject
import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject 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.rx3.asFlow
import org.signal.core.util.concurrent.subscribeWithSubject import org.signal.core.util.concurrent.subscribeWithSubject
import org.signal.core.util.orNull import org.signal.core.util.orNull
import org.signal.paging.ProxyPagingController import org.signal.paging.ProxyPagingController
import org.thoughtcrime.securesms.banner.Banner
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 import org.thoughtcrime.securesms.components.reminder.Reminder
import org.thoughtcrime.securesms.contactshare.Contact import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.conversation.ConversationMessage
@@ -42,6 +58,7 @@ import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
import org.thoughtcrime.securesms.conversation.v2.items.ChatColorsDrawable import org.thoughtcrime.securesms.conversation.v2.items.ChatColorsDrawable
import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.MessageTable import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.IdentityRecord import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.database.model.Mention import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageId
@@ -155,6 +172,8 @@ class ConversationViewModel(
private val refreshReminder: Subject<Unit> = PublishSubject.create() private val refreshReminder: Subject<Unit> = PublishSubject.create()
val reminder: Observable<Optional<Reminder>> val reminder: Observable<Optional<Reminder>>
private val groupRecordFlow: Flow<GroupRecord?>
private val refreshIdentityRecords: Subject<Unit> = PublishSubject.create() private val refreshIdentityRecords: Subject<Unit> = PublishSubject.create()
private val identityRecordsStore: RxStore<IdentityRecordsState> = RxStore(IdentityRecordsState()) private val identityRecordsStore: RxStore<IdentityRecordsState> = RxStore(IdentityRecordsState())
val identityRecordsObservable: Observable<IdentityRecordsState> = identityRecordsStore.stateFlowable.toObservable() val identityRecordsObservable: Observable<IdentityRecordsState> = identityRecordsStore.stateFlowable.toObservable()
@@ -276,6 +295,8 @@ class ConversationViewModel(
.flatMapMaybe { groupRecord -> repository.getReminder(groupRecord.orNull()) } .flatMapMaybe { groupRecord -> repository.getReminder(groupRecord.orNull()) }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
groupRecordFlow = recipientRepository.groupRecord.subscribeOn(Schedulers.io()).asFlow().map { it.orNull() }
Observable.combineLatest( Observable.combineLatest(
refreshIdentityRecords.startWithItem(Unit).observeOn(Schedulers.io()), refreshIdentityRecords.startWithItem(Unit).observeOn(Schedulers.io()),
recipient, recipient,
@@ -299,6 +320,36 @@ 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 {
flow {
if (it == null) {
emit(PendingGroupJoinRequestsBanner(false, 0, {}, {}))
} else {
emitAll(Producer(it.actionableRequestingMembersCount, groupJoinClickListener).flow)
}
}
}
val groupV1SuggestionsFlow = groupRecordFlow.map {
if (it == null) {
GroupsV1MigrationSuggestionsBanner(0, {}, {})
} else {
GroupsV1MigrationSuggestionsBanner(it.gv1MigrationSuggestions.size, onAddMembers, onNoThanks)
}
}
return listOf(
OutdatedBuildBanner.createFlow(context, OutdatedBuildBanner.ExpiryStatus.EXPIRED_ONLY),
UnauthorizedBanner.createFlow(context),
ServiceOutageBanner.createFlow(context),
pendingGroupJoinFlow,
groupV1SuggestionsFlow,
BubbleOptOutBanner.createFlow(inBubble = true, bubbleClickListener)
)
}
fun onChatBoundsChanged(bounds: Rect) { fun onChatBoundsChanged(bounds: Rect) {
chatBounds.onNext(bounds) chatBounds.onNext(bounds)
} }

View File

@@ -97,8 +97,14 @@ import org.thoughtcrime.securesms.badges.self.expired.ExpiredOneTimeBadgeBottomS
import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment; import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment;
import org.thoughtcrime.securesms.banner.Banner; import org.thoughtcrime.securesms.banner.Banner;
import org.thoughtcrime.securesms.banner.BannerManager; 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.DozeBanner;
import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner; import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner;
import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner; import org.thoughtcrime.securesms.banner.banners.MediaRestoreProgressBanner;
import org.thoughtcrime.securesms.banner.banners.ServiceOutageBanner;
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner;
import org.thoughtcrime.securesms.banner.banners.UsernameOutOfSyncBanner;
import org.thoughtcrime.securesms.components.DeleteSyncEducationDialog; import org.thoughtcrime.securesms.components.DeleteSyncEducationDialog;
import org.thoughtcrime.securesms.components.Material3SearchToolbar; import org.thoughtcrime.securesms.components.Material3SearchToolbar;
import org.thoughtcrime.securesms.components.RatingManager; import org.thoughtcrime.securesms.components.RatingManager;
@@ -204,6 +210,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import kotlin.Unit; import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import kotlinx.coroutines.flow.Flow; import kotlinx.coroutines.flow.Flow;
import static android.app.Activity.RESULT_OK; import static android.app.Activity.RESULT_OK;
@@ -423,7 +430,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
initializeListAdapters(); initializeListAdapters();
initializeTypingObserver(); initializeTypingObserver();
initializeVoiceNotePlayer(); initializeVoiceNotePlayer();
if (RemoteConfig.newBannerUi()) {
initializeBanners(); initializeBanners();
maybeScheduleRefreshProfileJob();
}
RatingManager.showRatingDialogIfNecessary(requireContext()); RatingManager.showRatingDialogIfNecessary(requireContext());
@@ -882,12 +892,31 @@ public class ConversationListFragment extends MainFragment implements ActionMode
} }
private void initializeBanners() { private void initializeBanners() {
if (RemoteConfig.newBannerUi()) { final List<Flow<? extends Banner>> bannerRepositories = List.of(OutdatedBuildBanner.createFlow(requireContext(), OutdatedBuildBanner.ExpiryStatus.EXPIRED_ONLY),
final List<Flow<? extends Banner>> bannerRepositories = List.of(OutdatedBuildBanner.createFlow(requireContext()), UnauthorizedBanner.createFlow(requireContext()),
ServiceOutageBanner.createFlow(requireContext()),
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())); MediaRestoreProgressBanner.createLifecycleAwareFlow(getViewLifecycleOwner()));
final BannerManager bannerManager = new BannerManager(bannerRepositories); final BannerManager bannerManager = new BannerManager(bannerRepositories);
bannerManager.setContent(bannerView.get()); bannerManager.setContent(bannerView.get());
} }
private void maybeScheduleRefreshProfileJob() {
switch (SignalStore.account().getUsernameSyncState()) {
case USERNAME_AND_LINK_CORRUPTED, LINK_CORRUPTED -> AppDependencies.getJobManager().add(new RefreshOwnProfileJob());
case IN_SYNC -> {}
}
} }
private @NonNull VoiceNotePlayerView requireVoiceNotePlayerView() { private @NonNull VoiceNotePlayerView requireVoiceNotePlayerView() {

View File

@@ -6,7 +6,6 @@
package org.thoughtcrime.securesms.registration.data package org.thoughtcrime.securesms.registration.data
import okio.ByteString.Companion.toByteString import okio.ByteString.Companion.toByteString
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.state.KyberPreKeyRecord import org.signal.libsignal.protocol.state.KyberPreKeyRecord
import org.signal.libsignal.protocol.state.SignedPreKeyRecord import org.signal.libsignal.protocol.state.SignedPreKeyRecord

View File

@@ -11,6 +11,7 @@ import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.compose.ui.platform.ComposeView
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.SharedElementCallback import androidx.core.app.SharedElementCallback
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
@@ -29,6 +30,9 @@ import org.greenrobot.eventbus.ThreadMode
import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.concurrent.LifecycleDisposable
import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.BannerManager
import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner
import org.thoughtcrime.securesms.components.Material3SearchToolbar import org.thoughtcrime.securesms.components.Material3SearchToolbar
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder
import org.thoughtcrime.securesms.components.reminder.Reminder import org.thoughtcrime.securesms.components.reminder.Reminder
@@ -62,6 +66,7 @@ import org.thoughtcrime.securesms.stories.tabs.ConversationListTab
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel
import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity
import org.thoughtcrime.securesms.util.PlayStoreUtil import org.thoughtcrime.securesms.util.PlayStoreUtil
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.fragments.requireListener import org.thoughtcrime.securesms.util.fragments.requireListener
@@ -82,6 +87,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
private lateinit var cameraFab: FloatingActionButton private lateinit var cameraFab: FloatingActionButton
private lateinit var reminderView: Stub<ReminderView> private lateinit var reminderView: Stub<ReminderView>
private lateinit var bannerView: Stub<ComposeView>
private val lifecycleDisposable = LifecycleDisposable() private val lifecycleDisposable = LifecycleDisposable()
@@ -144,6 +150,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
reminderView = ViewUtil.findStubById(view, R.id.reminder) reminderView = ViewUtil.findStubById(view, R.id.reminder)
bannerView = ViewUtil.findStubById(view, R.id.banner_stub)
updateReminders() updateReminders()
} }
@@ -153,6 +160,14 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
} }
private fun updateReminders() { private fun updateReminders() {
if (RemoteConfig.newBannerUi) {
val bannerFlows = listOf(
OutdatedBuildBanner.createFlow(requireContext(), OutdatedBuildBanner.ExpiryStatus.EXPIRED_ONLY),
UnauthorizedBanner.createFlow(requireContext())
)
val bannerManager = BannerManager(bannerFlows)
bannerManager.setContent(bannerView.get())
} else {
if (ExpiredBuildReminder.isEligible()) { if (ExpiredBuildReminder.isEligible()) {
showReminder(ExpiredBuildReminder(context)) showReminder(ExpiredBuildReminder(context))
} else if (UnauthorizedReminder.isEligible(context)) { } else if (UnauthorizedReminder.isEligible(context)) {
@@ -161,6 +176,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
hideReminders() hideReminders()
} }
} }
}
private fun showReminder(reminder: Reminder) { private fun showReminder(reminder: Reminder) {
if (!reminderView.resolved()) { if (!reminderView.resolved()) {

View File

@@ -27,4 +27,12 @@
android:layout="@layout/conversation_activity_reminderview_stub" android:layout="@layout/conversation_activity_reminderview_stub"
app:layout_constraintTop_toTopOf="@id/recycler"/> app:layout_constraintTop_toTopOf="@id/recycler"/>
<ViewStub
android:id="@+id/banner_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/banner_compose_view"
android:layout="@layout/conversation_list_banner_view"
app:layout_constraintTop_toTopOf="@id/recycler"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -108,4 +108,12 @@
android:layout="@layout/stories_landing_reminder_view" android:layout="@layout/stories_landing_reminder_view"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ViewStub
android:id="@+id/banner_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/banner_compose_view"
android:layout="@layout/conversation_list_banner_view"
app:layout_constraintTop_toTopOf="@id/recycler"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -180,6 +180,13 @@
android:inflatedId="@+id/review_banner" android:inflatedId="@+id/review_banner"
android:layout="@layout/review_banner_view" /> android:layout="@layout/review_banner_view" />
<ViewStub
android:id="@+id/banner_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/banner_compose_view"
android:layout="@layout/conversation_list_banner_view" />
</org.thoughtcrime.securesms.conversation.v2.ConversationBannerView> </org.thoughtcrime.securesms.conversation.v2.ConversationBannerView>
</FrameLayout> </FrameLayout>