diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt index 7584b43f74..843057352f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt @@ -252,16 +252,23 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner ) ) + if (SignalStore.internal.largeScreenUi) { + LaunchedEffect(scaffoldNavigator.currentDestination) { + if (scaffoldNavigator.currentDestination?.pane == ThreePaneScaffoldRole.Secondary) { + mainNavigationViewModel.goTo(MainNavigationDetailLocation.Empty) + } + } + } + LaunchedEffect(detailLocation) { if (detailLocation is MainNavigationDetailLocation.Conversation) { if (SignalStore.internal.largeScreenUi) { scaffoldNavigator.navigateTo(ThreePaneScaffoldRole.Primary, detailLocation) } else { startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent) + mainNavigationViewModel.goTo(MainNavigationDetailLocation.Empty) } } - - mainNavigationViewModel.goTo(MainNavigationDetailLocation.Empty) } AppScaffold( diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt index 6177759d4c..9f1e15fde2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchMediator.kt @@ -126,6 +126,8 @@ class ContactSearchMediator( } } + fun getFilter(): String? = viewModel.getQuery() + fun onConversationFilterRequestChanged(conversationFilterRequest: ConversationFilterRequest) { viewModel.setConversationFilterRequest(conversationFilterRequest) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt index 6209088b5b..c316a9e85b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/paged/ContactSearchViewModel.kt @@ -1,9 +1,10 @@ package org.thoughtcrime.securesms.contacts.paged +import androidx.lifecycle.AbstractSavedStateViewModelFactory import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.map import androidx.lifecycle.switchMap import io.reactivex.rxjava3.core.Observable @@ -26,6 +27,7 @@ import org.whispersystems.signalservice.api.util.Preconditions * Simple, reusable view model that manages a ContactSearchPagedDataSource as well as filter and expansion state. */ class ContactSearchViewModel( + private val savedStateHandle: SavedStateHandle, private val selectionLimits: SelectionLimits, private val contactSearchRepository: ContactSearchRepository, private val performSafetyNumberChecks: Boolean, @@ -34,6 +36,10 @@ class ContactSearchViewModel( private val contactSearchPagedDataSourceRepository: ContactSearchPagedDataSourceRepository ) : ViewModel() { + companion object { + private const val QUERY = "query" + } + private val safetyNumberRepository: SafetyNumberRepository by lazy { SafetyNumberRepository() } private val disposables = CompositeDisposable() @@ -45,7 +51,7 @@ class ContactSearchViewModel( .build() private val pagedData = MutableLiveData>() - private val configurationStore = Store(ContactSearchState()) + private val configurationStore = Store(ContactSearchState(query = savedStateHandle[QUERY])) private val selectionStore = Store>(emptySet()) private val errorEvents = PublishSubject.create() @@ -73,7 +79,10 @@ class ContactSearchViewModel( pagedData.value = PagedData.createForLiveData(pagedDataSource, pagingConfig) } + fun getQuery(): String? = savedStateHandle[QUERY] + fun setQuery(query: String?) { + savedStateHandle[QUERY] = query configurationStore.update { it.copy(query = query) } } @@ -169,10 +178,11 @@ class ContactSearchViewModel( private val arbitraryRepository: ArbitraryRepository?, private val searchRepository: SearchRepository, private val contactSearchPagedDataSourceRepository: ContactSearchPagedDataSourceRepository - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { + ) : AbstractSavedStateViewModelFactory() { + override fun create(key: String, modelClass: Class, handle: SavedStateHandle): T { return modelClass.cast( ContactSearchViewModel( + savedStateHandle = handle, selectionLimits = selectionLimits, contactSearchRepository = repository, performSafetyNumberChecks = performSafetyNumberChecks, 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 f7820dad35..93d040ebb7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -389,6 +389,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode initializeVoiceNotePlayer(); initializeBanners(); maybeScheduleRefreshProfileJob(); + ConversationListFragmentExtensionsKt.listenToEventBusWhileResumed(this, mainNavigationViewModel.getDetailLocation()); + + String query = contactSearchMediator.getFilter(); + if (query != null) { + onSearchQueryUpdated(query); + } RatingManager.showRatingDialogIfNecessary(requireContext()); @@ -473,7 +479,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode initializeSearchListener(); initializeFilterListener(); - EventBus.getDefault().register(this); itemAnimator.disable(); SpoilerAnnotation.resetRevealedSpoilers(); @@ -539,13 +544,6 @@ public class ConversationListFragment extends MainFragment implements ActionMode itemAnimator.disable(); } - @Override - public void onPause() { - super.onPause(); - - EventBus.getDefault().unregister(this); - } - @Override public void onStop() { super.onStop(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragmentExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragmentExtensions.kt new file mode 100644 index 0000000000..d1b37d52d2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragmentExtensions.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversationlist + +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.eventFlow +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus +import org.thoughtcrime.securesms.main.MainNavigationDetailLocation +import org.thoughtcrime.securesms.window.WindowSizeClass.Companion.getWindowSizeClass + +/** + * When the user searches for a conversation and then enters a message, we should clear + * the search. This is driven by an event bus, which we want to subscribe to only when + * the screen has been resumed. + * + * On COMPACT form factor specifically, we also need to wait until we are in the EMPTY + * detail location, to avoid weird predictive back animation issues. + * + * On other screen types, since we are in a multi-pane mode, we can subscribe immediately. + */ +fun Fragment.listenToEventBusWhileResumed( + detailLocation: Flow +) { + lifecycleScope.launch { + detailLocation + .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.RESUMED) + .collectLatest { + if (resources.getWindowSizeClass().isCompact()) { + when (it) { + is MainNavigationDetailLocation.Conversation -> unsubscribe() + MainNavigationDetailLocation.Empty -> subscribe() + } + } else { + subscribe() + } + } + } + + lifecycleScope.launch { + lifecycle.eventFlow.filter { it == Lifecycle.Event.ON_PAUSE } + .collectLatest { unsubscribe() } + } +} + +private fun Fragment.subscribe() { + if (!EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().register(this) + } +} + +private fun Fragment.unsubscribe() { + if (EventBus.getDefault().isRegistered(this)) { + EventBus.getDefault().unregister(this) + } +}