diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsPermanentErrorBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsPermanentErrorBanner.kt index 3b119cd169..52fa4135c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsPermanentErrorBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsPermanentErrorBanner.kt @@ -46,8 +46,8 @@ class CdsPermanentErrorBanner(private val fragmentManager: FragmentManager) : Ba val PERMANENT_TIME_CUTOFF = 30.days.inWholeMilliseconds @JvmStatic - fun createFlow(fragmentManager: FragmentManager): Flow = createAndEmit { - CdsPermanentErrorBanner(fragmentManager) + fun createFlow(childFragmentManager: FragmentManager): Flow = createAndEmit { + CdsPermanentErrorBanner(childFragmentManager) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsTemporaryErrorBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsTemporaryErrorBanner.kt index e1c4db1ee0..adebcded25 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsTemporaryErrorBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsTemporaryErrorBanner.kt @@ -39,8 +39,8 @@ class CdsTemporaryErrorBanner(private val fragmentManager: FragmentManager) : Ba companion object { @JvmStatic - fun createFlow(fragmentManager: FragmentManager): Flow = createAndEmit { - CdsTemporaryErrorBanner(fragmentManager) + fun createFlow(childFragmentManager: FragmentManager): Flow = createAndEmit { + CdsTemporaryErrorBanner(childFragmentManager) } } } 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 41721b544e..1a225bdd07 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 @@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.banner.banners import android.content.Context import android.os.Build -import androidx.annotation.RequiresApi import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource 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.TextSecurePreferences -@RequiresApi(23) 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 override fun DisplayBanner() { + 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), @@ -45,11 +47,7 @@ class DozeBanner(private val context: Context) : Banner() { @JvmStatic fun createFlow(context: Context): Flow = createAndEmit { - if (Build.VERSION.SDK_INT >= 23) { - DozeBanner(context) - } else { - null - } + DozeBanner(context) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt index a56b549aa8..b591c067e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt @@ -12,7 +12,7 @@ import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -40,7 +40,9 @@ class MediaRestoreProgressBanner(private val data: MediaRestoreEvent) : Banner() lifecycleOwner.lifecycle.addObserver(observer) return observer.flow } else { - return emptyFlow() + return flow { + emit(MediaRestoreProgressBanner(MediaRestoreEvent(0L, 0L))) + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/OutdatedBuildBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/OutdatedBuildBanner.kt index a085686bc6..5dd179e849 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/OutdatedBuildBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/OutdatedBuildBanner.kt @@ -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. + * + * @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 override fun DisplayBanner() { - DefaultBanner( - title = null, - body = if (SignalStore.misc.isClientDeprecated) { + 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 { @@ -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 { private const val MAX_DAYS_UNTIL_EXPIRE = 10 @JvmStatic - fun createFlow(context: Context): Flow = createAndEmit { + fun createFlow(context: Context, status: ExpiryStatus): Flow = createAndEmit { val daysUntilExpiry = Util.getTimeUntilBuildExpiry(SignalStore.misc.estimatedServerTime).milliseconds.inWholeDays.toInt() - OutdatedBuildBanner(context, daysUntilExpiry) + OutdatedBuildBanner(context, daysUntilExpiry, status) } } } 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 206ce14c48..82d33bd3e1 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 @@ -9,7 +9,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.pluralStringResource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flow import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.banner.Banner import org.thoughtcrime.securesms.banner.ui.compose.Action @@ -33,14 +32,8 @@ class PendingGroupJoinRequestsBanner(override val enabled: Boolean, private val ) } - companion object { - - @JvmStatic - fun createFlow(suggestionsSize: Int, onViewClicked: () -> Unit): Flow = Producer(suggestionsSize, onViewClicked).flow - } - - private class Producer(suggestionsSize: Int, onViewClicked: () -> Unit) { - val dismissListener: () -> Unit = { + class Producer(suggestionsSize: Int, onViewClicked: () -> Unit) { + private val dismissListener: () -> Unit = { mutableStateFlow.tryEmit(PendingGroupJoinRequestsBanner(false, suggestionsSize, onViewClicked, null)) } private val mutableStateFlow: MutableStateFlow = MutableStateFlow(PendingGroupJoinRequestsBanner(true, suggestionsSize, onViewClicked, dismissListener)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UsernameOutOfSyncBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UsernameOutOfSyncBanner.kt index b93025fd6e..3425c6180a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UsernameOutOfSyncBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UsernameOutOfSyncBanner.kt @@ -46,6 +46,9 @@ class UsernameOutOfSyncBanner(private val context: Context, private val username companion object { + /** + * @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 = createAndEmit { UsernameOutOfSyncBanner(context, SignalStore.account.usernameSyncState, onActionClick) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt index 049167a50c..6dc05eec9a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsFragment.kt @@ -6,6 +6,7 @@ import android.widget.TextView import android.widget.Toast import androidx.annotation.IdRes import androidx.annotation.StringRes +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController @@ -18,6 +19,9 @@ import org.greenrobot.eventbus.ThreadMode 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.UnauthorizedBanner import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.components.emoji.EmojiTextView import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder @@ -59,12 +63,14 @@ class AppSettingsFragment : DSLSettingsFragment( private val viewModel: AppSettingsViewModel by viewModels() private lateinit var reminderView: Stub + private lateinit var bannerView: Stub override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewLifecycleOwner.lifecycle.addObserver(InAppPaymentsBottomSheetDelegate(childFragmentManager, viewLifecycleOwner)) super.onViewCreated(view, savedInstanceState) reminderView = ViewUtil.findStubById(view, R.id.reminder_stub) + bannerView = ViewUtil.findStubById(view, R.id.banner_stub) updateReminders() } @@ -85,12 +91,21 @@ class AppSettingsFragment : DSLSettingsFragment( } private fun updateReminders() { - if (ExpiredBuildReminder.isEligible()) { - showReminder(ExpiredBuildReminder(context)) - } else if (UnauthorizedReminder.isEligible(context)) { - showReminder(UnauthorizedReminder()) + 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 { - hideReminders() + if (ExpiredBuildReminder.isEligible()) { + showReminder(ExpiredBuildReminder(context)) + } else if (UnauthorizedReminder.isEligible(context)) { + showReminder(UnauthorizedReminder()) + } else { + hideReminders() + } } viewModel.refreshDeprecatedOrUnregistered() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationBannerView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationBannerView.kt index 738e0d7a44..f7b00277eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationBannerView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationBannerView.kt @@ -14,8 +14,12 @@ import android.util.AttributeSet import android.view.Gravity 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 import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView import org.thoughtcrime.securesms.components.reminder.Reminder import org.thoughtcrime.securesms.components.reminder.ReminderView @@ -47,6 +51,7 @@ class ConversationBannerView @JvmOverloads constructor( ) : LinearLayoutCompat(context, attrs, defStyleAttr) { private val unverifiedBannerStub: Stub by lazy { ViewUtil.findStubById(this, R.id.unverified_banner_stub) } private val reminderStub: Stub by lazy { ViewUtil.findStubById(this, R.id.reminder_stub) } + private val bannerStub: Stub by lazy { ViewUtil.findStubById(this, R.id.banner_stub) } private val reviewBannerStub: Stub by lazy { ViewUtil.findStubById(this, R.id.review_banner_stub) } private val voiceNotePlayerStub: Stub by lazy { ViewUtil.findStubById(this, R.id.voice_note_player_stub) } @@ -56,6 +61,13 @@ class ConversationBannerView @JvmOverloads constructor( orientation = VERTICAL } + fun collectAndShowBanners(flows: Iterable>) { + val bannerManager = BannerManager(flows) + show(stub = bannerStub) { + bannerManager.setContent(this) + } + } + fun showReminder(reminder: Reminder) { show( stub = reminderStub diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index e4ca5db05f..e1dfc462fb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -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.v2.GroupBlockJoinRequestResult import org.thoughtcrime.securesms.invites.InviteActions +import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob import org.thoughtcrime.securesms.keyboard.KeyboardPage import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel @@ -1016,17 +1017,38 @@ class ConversationFragment : VoiceMessageRecordingSessionCallbacks() ) - binding.conversationBanner.listener = ConversationBannerListener() - viewModel - .reminder - .subscribeBy { reminder -> - if (reminder.isPresent) { - binding.conversationBanner.showReminder(reminder.get()) - } else { - binding.conversationBanner.clearReminder() - } + 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()) } - .addTo(disposables) + } else { + viewModel + .reminder + .subscribeBy { reminder -> + if (reminder.isPresent) { + binding.conversationBanner.showReminder(reminder.get()) + } else { + binding.conversationBanner.clearReminder() + } + } + .addTo(disposables) + } viewModel .identityRecordsObservable 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 416871a6b4..e3a809fc16 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 @@ -29,9 +29,25 @@ 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.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.orNull 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.contactshare.Contact 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.database.DatabaseObserver 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.Mention import org.thoughtcrime.securesms.database.model.MessageId @@ -155,6 +172,8 @@ class ConversationViewModel( private val refreshReminder: Subject = PublishSubject.create() val reminder: Observable> + private val groupRecordFlow: Flow + private val refreshIdentityRecords: Subject = PublishSubject.create() private val identityRecordsStore: RxStore = RxStore(IdentityRecordsState()) val identityRecordsObservable: Observable = identityRecordsStore.stateFlowable.toObservable() @@ -276,6 +295,8 @@ class ConversationViewModel( .flatMapMaybe { groupRecord -> repository.getReminder(groupRecord.orNull()) } .observeOn(AndroidSchedulers.mainThread()) + groupRecordFlow = recipientRepository.groupRecord.subscribeOn(Schedulers.io()).asFlow().map { it.orNull() } + Observable.combineLatest( refreshIdentityRecords.startWithItem(Unit).observeOn(Schedulers.io()), recipient, @@ -299,6 +320,36 @@ class ConversationViewModel( }) } + @OptIn(ExperimentalCoroutinesApi::class) + fun getBannerFlows(context: Context, groupJoinClickListener: () -> Unit, onAddMembers: () -> Unit, onNoThanks: () -> Unit, bubbleClickListener: (Boolean) -> Unit): List> { + 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) { chatBounds.onNext(bounds) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 10b2a91b68..c3a236329a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -97,8 +97,14 @@ import org.thoughtcrime.securesms.badges.self.expired.ExpiredOneTimeBadgeBottomS import org.thoughtcrime.securesms.badges.self.expired.MonthlyDonationCanceledBottomSheetDialogFragment; 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.DozeBanner; import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner; 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.Material3SearchToolbar; import org.thoughtcrime.securesms.components.RatingManager; @@ -204,6 +210,7 @@ import java.util.Set; import java.util.stream.Collectors; import kotlin.Unit; +import kotlin.jvm.functions.Function1; import kotlinx.coroutines.flow.Flow; import static android.app.Activity.RESULT_OK; @@ -423,7 +430,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode initializeListAdapters(); initializeTypingObserver(); initializeVoiceNotePlayer(); - initializeBanners(); + if (RemoteConfig.newBannerUi()) { + initializeBanners(); + maybeScheduleRefreshProfileJob(); + } RatingManager.showRatingDialogIfNecessary(requireContext()); @@ -882,11 +892,30 @@ public class ConversationListFragment extends MainFragment implements ActionMode } private void initializeBanners() { - if (RemoteConfig.newBannerUi()) { - final List> bannerRepositories = List.of(OutdatedBuildBanner.createFlow(requireContext()), - MediaRestoreProgressBanner.createLifecycleAwareFlow(getViewLifecycleOwner())); - final BannerManager bannerManager = new BannerManager(bannerRepositories); - bannerManager.setContent(bannerView.get()); + final List> bannerRepositories = List.of(OutdatedBuildBanner.createFlow(requireContext(), OutdatedBuildBanner.ExpiryStatus.EXPIRED_ONLY), + 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())); + final BannerManager bannerManager = new BannerManager(bannerRepositories); + 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 -> {} } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/data/LocalRegistrationMetadataUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/data/LocalRegistrationMetadataUtil.kt index 4eaa4dd31a..021e78ed83 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/data/LocalRegistrationMetadataUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/data/LocalRegistrationMetadataUtil.kt @@ -6,7 +6,6 @@ package org.thoughtcrime.securesms.registration.data import okio.ByteString.Companion.toByteString -import org.signal.libsignal.protocol.IdentityKey import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.state.KyberPreKeyRecord import org.signal.libsignal.protocol.state.SignedPreKeyRecord @@ -44,11 +43,11 @@ object LocalRegistrationMetadataUtil { }.build() } - fun LocalRegistrationMetadata.getAciIdentityKeyPair() : IdentityKeyPair { + fun LocalRegistrationMetadata.getAciIdentityKeyPair(): IdentityKeyPair { return IdentityKeyPair(aciIdentityKeyPair.toByteArray()) } - fun LocalRegistrationMetadata.getPniIdentityKeyPair() : IdentityKeyPair { + fun LocalRegistrationMetadata.getPniIdentityKeyPair(): IdentityKeyPair { return IdentityKeyPair(pniIdentityKeyPair.toByteArray()) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt index 687bdc7951..b221cfd13a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/ui/RegistrationViewModel.kt @@ -399,7 +399,7 @@ class RegistrationViewModel : ViewModel() { allowedToRequestCode = networkResult.body.allowedToRequestCode, challengesRequested = Challenge.parse(networkResult.body.requestedInformation), verified = networkResult.body.verified, - inProgress = false + inProgress = false ) } }, @@ -408,7 +408,7 @@ class RegistrationViewModel : ViewModel() { store.update { it.copy( sessionCreationError = error, - inProgress = false + inProgress = false ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt index eb36c63098..f298ad6d7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt @@ -11,6 +11,7 @@ import android.view.View import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.annotation.IdRes +import androidx.compose.ui.platform.ComposeView import androidx.core.app.ActivityOptionsCompat import androidx.core.app.SharedElementCallback import androidx.core.view.ViewCompat @@ -29,6 +30,9 @@ import org.greenrobot.eventbus.ThreadMode 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.UnauthorizedBanner import org.thoughtcrime.securesms.components.Material3SearchToolbar import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder 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.viewer.StoryViewerActivity import org.thoughtcrime.securesms.util.PlayStoreUtil +import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter 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 reminderView: Stub + private lateinit var bannerView: Stub private val lifecycleDisposable = LifecycleDisposable() @@ -144,6 +150,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l super.onViewCreated(view, savedInstanceState) reminderView = ViewUtil.findStubById(view, R.id.reminder) + bannerView = ViewUtil.findStubById(view, R.id.banner_stub) updateReminders() } @@ -153,12 +160,21 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l } private fun updateReminders() { - if (ExpiredBuildReminder.isEligible()) { - showReminder(ExpiredBuildReminder(context)) - } else if (UnauthorizedReminder.isEligible(context)) { - showReminder(UnauthorizedReminder()) + 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 { - hideReminders() + if (ExpiredBuildReminder.isEligible()) { + showReminder(ExpiredBuildReminder(context)) + } else if (UnauthorizedReminder.isEligible(context)) { + showReminder(UnauthorizedReminder()) + } else { + hideReminders() + } } } diff --git a/app/src/main/res/layout/dsl_settings_fragment_with_reminder.xml b/app/src/main/res/layout/dsl_settings_fragment_with_reminder.xml index f25e4c0b19..018a86afda 100644 --- a/app/src/main/res/layout/dsl_settings_fragment_with_reminder.xml +++ b/app/src/main/res/layout/dsl_settings_fragment_with_reminder.xml @@ -27,4 +27,12 @@ android:layout="@layout/conversation_activity_reminderview_stub" app:layout_constraintTop_toTopOf="@id/recycler"/> + + \ No newline at end of file diff --git a/app/src/main/res/layout/stories_landing_fragment.xml b/app/src/main/res/layout/stories_landing_fragment.xml index 74763c85ea..7830fcb510 100644 --- a/app/src/main/res/layout/stories_landing_fragment.xml +++ b/app/src/main/res/layout/stories_landing_fragment.xml @@ -108,4 +108,12 @@ android:layout="@layout/stories_landing_reminder_view" app:layout_constraintTop_toTopOf="parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/v2_conversation_fragment.xml b/app/src/main/res/layout/v2_conversation_fragment.xml index d5ded2a141..d3005273e7 100644 --- a/app/src/main/res/layout/v2_conversation_fragment.xml +++ b/app/src/main/res/layout/v2_conversation_fragment.xml @@ -180,6 +180,13 @@ android:inflatedId="@+id/review_banner" android:layout="@layout/review_banner_view" /> + +