mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-19 16:24:41 +01:00
Utilize events instead of direct calls in main nav view model.
This commit is contained in:
@@ -82,6 +82,7 @@ import kotlinx.coroutines.withContext
|
||||
import org.signal.core.ui.BottomSheetUtil
|
||||
import org.signal.core.ui.NavigationType
|
||||
import org.signal.core.ui.compose.Snackbars
|
||||
import org.signal.core.ui.compose.split.ListDetailEvents
|
||||
import org.signal.core.ui.compose.split.ListDetailNavDisplay
|
||||
import org.signal.core.ui.compose.split.ListDetailPaneLayout
|
||||
import org.signal.core.ui.compose.split.ListDetailPaneMetrics
|
||||
@@ -145,8 +146,9 @@ import org.thoughtcrime.securesms.main.MainDetailRoute
|
||||
import org.thoughtcrime.securesms.main.MainListRoute
|
||||
import org.thoughtcrime.securesms.main.MainMegaphoneState
|
||||
import org.thoughtcrime.securesms.main.MainNavigationBar
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEventSink
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEvents
|
||||
import org.thoughtcrime.securesms.main.MainNavigationRail
|
||||
import org.thoughtcrime.securesms.main.MainNavigationRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel
|
||||
import org.thoughtcrime.securesms.main.MainSnackbar
|
||||
import org.thoughtcrime.securesms.main.MainSnackbarHostKey
|
||||
@@ -188,7 +190,7 @@ class MainActivity :
|
||||
MainNavigator.NavigatorProvider,
|
||||
Material3OnScrollHelperBinder,
|
||||
ConversationListFragment.Callback,
|
||||
MainNavigationRouter,
|
||||
MainNavigationEventSink,
|
||||
CallLogFragment.Callback,
|
||||
GooglePayComponent {
|
||||
|
||||
@@ -263,7 +265,7 @@ class MainActivity :
|
||||
|
||||
private val mainBottomChromeCallback = BottomChromeCallback()
|
||||
private val megaphoneActionController = MainMegaphoneActionController()
|
||||
private val mainNavigationCallback = MainNavigationCallback()
|
||||
private val mainNavigationCallback: (MainListRoute) -> Unit = { mainNavigationViewModel.onEvent(MainNavigationEvents.GoToTab(it)) }
|
||||
|
||||
override val googlePayRepository: GooglePayRepository by lazy { GooglePayRepository(this) }
|
||||
override val googlePayResultPublisher: Subject<GooglePayComponent.GooglePayResult> = PublishSubject.create()
|
||||
@@ -287,7 +289,7 @@ class MainActivity :
|
||||
|
||||
AppForegroundObserver.addListener(object : AppForegroundObserver.Listener {
|
||||
override fun onForeground() {
|
||||
mainNavigationViewModel.getNextMegaphone()
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.RequestNextMegaphone)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -397,7 +399,7 @@ class MainActivity :
|
||||
val isBackHandlerEnabled = mainToolbarState.destination != MainListRoute.Chats && !isActionModeActive && !isSearchModeActive
|
||||
|
||||
BackHandler(enabled = isBackHandlerEnabled) {
|
||||
mainNavigationViewModel.goTo(MainListRoute.Chats)
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToList(MainListRoute.Chats))
|
||||
}
|
||||
|
||||
BackHandler(enabled = isActionModeActive) {
|
||||
@@ -491,8 +493,8 @@ class MainActivity :
|
||||
entries = tabEntries,
|
||||
isSplitPane = isSplitPane,
|
||||
paneAnchor = paneAnchor,
|
||||
onBack = { mainNavigationViewModel.popCurrentDetailLocation() },
|
||||
onExitDetail = { mainNavigationViewModel.exitDetailLocation() },
|
||||
onBack = { mainNavigationViewModel.onEvent(MainNavigationEvents.ListDetailEvent(ListDetailEvents.Back)) },
|
||||
onExitDetail = { mainNavigationViewModel.onEvent(MainNavigationEvents.ExitDetail) },
|
||||
layout = paneLayout,
|
||||
listPaneChrome = listPaneChrome,
|
||||
emptyDetailContent = emptyDetailContent,
|
||||
@@ -540,7 +542,7 @@ class MainActivity :
|
||||
return rememberListDetailPaneLayout(
|
||||
paneAnchor = paneAnchor,
|
||||
maxWidth = maxWidth,
|
||||
onAnchorSelected = { mainNavigationViewModel.onPaneAnchorSelected(it) },
|
||||
onAnchorSelected = { mainNavigationViewModel.onEvent(MainNavigationEvents.ListDetailEvent(ListDetailEvents.AnchorSelected(it))) },
|
||||
metrics = contentLayoutData,
|
||||
// Searching hides the rail, leaving nothing of the list pane behind once the detail fills the window.
|
||||
collapsedListWidth = when {
|
||||
@@ -690,29 +692,20 @@ class MainActivity :
|
||||
val extras = intent.extras ?: return
|
||||
|
||||
if (extras.getBoolean(KEY_EXIT_DETAIL, false)) {
|
||||
mainNavigationViewModel.exitDetailLocation()
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.ExitDetail)
|
||||
return
|
||||
}
|
||||
|
||||
val detailLocation = extras.getParcelableCompat(KEY_DETAIL_LOCATION, MainDetailRoute::class.java)
|
||||
if (detailLocation != null) {
|
||||
goTo(detailLocation)
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToDetail(detailLocation))
|
||||
return
|
||||
}
|
||||
|
||||
val startingTab = extras.getSerializableCompat(KEY_STARTING_TAB, MainListRoute::class.java)
|
||||
val startingTab = extras.getSerializableCompat(KEY_STARTING_TAB, MainListRoute::class.java) ?: return
|
||||
|
||||
when (startingTab) {
|
||||
MainListRoute.Chats -> mainNavigationViewModel.onChatsSelected()
|
||||
MainListRoute.Archive -> mainNavigationViewModel.onArchiveSelected()
|
||||
MainListRoute.Calls -> mainNavigationViewModel.onCallsSelected()
|
||||
MainListRoute.Stories -> {
|
||||
if (Stories.isFeatureEnabled()) {
|
||||
mainNavigationViewModel.onStoriesSelected()
|
||||
}
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
if (startingTab != MainListRoute.Stories || Stories.isFeatureEnabled()) {
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToTab(startingTab))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -749,7 +742,7 @@ class MainActivity :
|
||||
}
|
||||
|
||||
vitalsViewModel.checkSlowNotificationHeuristics()
|
||||
mainNavigationViewModel.refreshNavigationBarState()
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.RefreshNavigationBar)
|
||||
|
||||
CallQuality.consumeQualityRequest()?.let {
|
||||
CallQualityBottomSheetFragment.create(it).show(supportFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
@@ -775,7 +768,7 @@ class MainActivity :
|
||||
|
||||
if (resultCode == RESULT_OK && requestCode == CreateSvrPinActivity.REQUEST_NEW_PIN) {
|
||||
mainNavigationViewModel.snackbarRegistry.emit(SnackbarState(message = getString(R.string.ConfirmKbsPinFragment__pin_created), hostKey = MainSnackbarHostKey.MainChrome))
|
||||
mainNavigationViewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL)
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.MegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL))
|
||||
}
|
||||
|
||||
if (resultCode == RESULT_OK && requestCode == UsernameEditFragment.REQUEST_CODE) {
|
||||
@@ -796,7 +789,7 @@ class MainActivity :
|
||||
hostKey = MainSnackbarHostKey.MainChrome
|
||||
)
|
||||
)
|
||||
mainNavigationViewModel.onMegaphoneSnoozed(Megaphones.Event.VERIFY_BACKUP_KEY)
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.MegaphoneSnoozed(Megaphones.Event.VERIFY_BACKUP_KEY))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,8 +889,8 @@ class MainActivity :
|
||||
return
|
||||
}
|
||||
|
||||
mainNavigationViewModel.goTo(MainListRoute.Chats)
|
||||
mainNavigationViewModel.goTo(MainDetailRoute.Conversation(ConversationIntents.readArgsFromBundle(extras)))
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToList(MainListRoute.Chats))
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.Conversation(ConversationIntents.readArgsFromBundle(extras))))
|
||||
intent.action = null
|
||||
setIntent(intent)
|
||||
}
|
||||
@@ -1033,7 +1026,7 @@ class MainActivity :
|
||||
}
|
||||
|
||||
override fun onOpenArchiveClick() {
|
||||
mainNavigationViewModel.onArchiveSelected()
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToTab(MainListRoute.Archive))
|
||||
}
|
||||
|
||||
override fun onStarredMessagesClick() {
|
||||
@@ -1069,11 +1062,11 @@ class MainActivity :
|
||||
}
|
||||
|
||||
override fun onStoryPrivacyClick() {
|
||||
mainNavigationViewModel.goTo(MainDetailRoute.Stories.PrivacySettings)
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.Stories.PrivacySettings))
|
||||
}
|
||||
|
||||
override fun onStoryArchiveClick() {
|
||||
mainNavigationViewModel.goTo(MainDetailRoute.Stories.Archive)
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.Stories.Archive))
|
||||
}
|
||||
|
||||
override fun onCloseSearchClick() {
|
||||
@@ -1125,7 +1118,7 @@ class MainActivity :
|
||||
}
|
||||
|
||||
override fun onMegaphoneVisible(megaphone: Megaphone) {
|
||||
mainNavigationViewModel.onMegaphoneVisible(megaphone)
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.MegaphoneVisible(megaphone))
|
||||
}
|
||||
|
||||
override fun onSnackbarDismissed() = Unit
|
||||
@@ -1154,11 +1147,11 @@ class MainActivity :
|
||||
}
|
||||
|
||||
override fun onMegaphoneSnooze(event: Megaphones.Event) {
|
||||
mainNavigationViewModel.onMegaphoneSnoozed(event)
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.MegaphoneSnoozed(event))
|
||||
}
|
||||
|
||||
override fun onMegaphoneCompleted(event: Megaphones.Event) {
|
||||
mainNavigationViewModel.onMegaphoneCompleted(event)
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.MegaphoneCompleted(event))
|
||||
}
|
||||
|
||||
override fun onMegaphoneDialogFragmentRequested(dialogFragment: DialogFragment) {
|
||||
@@ -1166,18 +1159,5 @@ class MainActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private inner class MainNavigationCallback : (MainListRoute) -> Unit {
|
||||
override fun invoke(location: MainListRoute) {
|
||||
when (location) {
|
||||
MainListRoute.Chats -> mainNavigationViewModel.onChatsSelected()
|
||||
MainListRoute.Calls -> mainNavigationViewModel.onCallsSelected()
|
||||
MainListRoute.Stories -> mainNavigationViewModel.onStoriesSelected()
|
||||
MainListRoute.Archive -> mainNavigationViewModel.onArchiveSelected()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun goTo(location: MainListRoute) = mainNavigationViewModel.goTo(location)
|
||||
override fun goTo(location: MainDetailRoute) = mainNavigationViewModel.goTo(location)
|
||||
override fun exitDetailLocation() = mainNavigationViewModel.exitDetailLocation()
|
||||
override fun onEvent(event: MainNavigationEvents) = mainNavigationViewModel.onEvent(event)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
|
||||
import org.thoughtcrime.securesms.main.MainDetailRoute;
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEvents;
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
@@ -50,7 +51,7 @@ public class MainNavigator {
|
||||
.withStartingPosition(startingPosition)
|
||||
.asIncognito(incognito)
|
||||
.toConversationArgs())
|
||||
.subscribe(args -> viewModel.goTo(new MainDetailRoute.Conversation(args)));
|
||||
.subscribe(args -> viewModel.onEvent(new MainNavigationEvents.GoToDetail(new MainDetailRoute.Conversation(args))));
|
||||
|
||||
lifecycleDisposable.add(disposable);
|
||||
}
|
||||
|
||||
+12
-9
@@ -47,7 +47,8 @@ import org.thoughtcrime.securesms.calls.links.CallLinks
|
||||
import org.thoughtcrime.securesms.calls.links.SignalCallRow
|
||||
import org.thoughtcrime.securesms.database.CallLinkTable
|
||||
import org.thoughtcrime.securesms.main.MainDetailRoute
|
||||
import org.thoughtcrime.securesms.main.MainNavigationCallDetailRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEventSink
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEvents
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
|
||||
@@ -63,7 +64,7 @@ fun CallLinkDetailsScreen(
|
||||
viewModel: CallLinkDetailsViewModel = viewModel {
|
||||
CallLinkDetailsViewModel(roomId)
|
||||
},
|
||||
router: MainNavigationCallDetailRouter = viewModel<MainNavigationViewModel>(viewModelStoreOwner = LocalActivity.current as ComponentActivity) {
|
||||
eventSink: MainNavigationEventSink = viewModel<MainNavigationViewModel>(viewModelStoreOwner = LocalActivity.current as ComponentActivity) {
|
||||
error("Should already be created.")
|
||||
}
|
||||
) {
|
||||
@@ -72,7 +73,7 @@ fun CallLinkDetailsScreen(
|
||||
DefaultCallLinkDetailsCallback(
|
||||
activity = activity,
|
||||
viewModel = viewModel,
|
||||
router = router
|
||||
eventSink = eventSink
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,7 +91,7 @@ fun CallLinkDetailsScreen(
|
||||
class DefaultCallLinkDetailsCallback(
|
||||
private val activity: FragmentActivity,
|
||||
private val viewModel: CallLinkDetailsViewModel,
|
||||
private val router: MainNavigationCallDetailRouter
|
||||
private val eventSink: MainNavigationEventSink
|
||||
) : CallLinkDetailsCallback {
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
@@ -113,10 +114,12 @@ class DefaultCallLinkDetailsCallback(
|
||||
}
|
||||
|
||||
override fun onEditNameClicked() {
|
||||
router.goToCallDetail(
|
||||
MainDetailRoute.Calls.CallLinks.EditCallLinkName(
|
||||
callLinkRoomId = viewModel.recipientSnapshot!!.requireCallLinkRoomId(),
|
||||
currentName = viewModel.nameSnapshot
|
||||
eventSink.onEvent(
|
||||
MainNavigationEvents.GoToDetail(
|
||||
MainDetailRoute.Calls.CallLinks.EditCallLinkName(
|
||||
callLinkRoomId = viewModel.recipientSnapshot!!.requireCallLinkRoomId(),
|
||||
currentName = viewModel.nameSnapshot
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -157,7 +160,7 @@ class DefaultCallLinkDetailsCallback(
|
||||
viewModel.setDisplayRevocationDialog(false)
|
||||
activity.lifecycleScope.launch {
|
||||
if (viewModel.delete()) {
|
||||
router.exitDetailLocation()
|
||||
eventSink.onEvent(MainNavigationEvents.ExitDetail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.databinding.CallLogFragmentBinding
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.main.MainDetailRoute
|
||||
import org.thoughtcrime.securesms.main.MainListRoute
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEvents
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel
|
||||
import org.thoughtcrime.securesms.main.MainSnackbarHostKey
|
||||
import org.thoughtcrime.securesms.main.MainToolbarMode
|
||||
@@ -335,7 +336,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
if (viewModel.selectionStateSnapshot.isNotEmpty(binding.recycler.adapter!!.itemCount)) {
|
||||
viewModel.toggleSelected(callLogRow.id)
|
||||
} else {
|
||||
mainNavigationViewModel.goTo(MainDetailRoute.CallLinkDetails(callLogRow.record.roomId))
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.CallLinkDetails(callLogRow.record.roomId)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,7 +391,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
}
|
||||
|
||||
override fun goToCallLinkDetails(roomId: CallLinkRoomId) {
|
||||
mainNavigationViewModel.goTo(MainDetailRoute.CallLinkDetails(roomId))
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.CallLinkDetails(roomId)))
|
||||
}
|
||||
|
||||
override fun deleteCall(call: CallLogRow) {
|
||||
|
||||
+9
-7
@@ -64,7 +64,8 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescription
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob
|
||||
import org.thoughtcrime.securesms.main.MainNavigationChatDetailRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEventSink
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEvents
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewCache
|
||||
@@ -117,7 +118,7 @@ class ConversationSettingsFragment : ComposeFragment() {
|
||||
)
|
||||
|
||||
private var transitionCallback: TransitionCallback? = null
|
||||
private var chatRouter: MainNavigationChatDetailRouter? = null
|
||||
private var mainNavigationEventSink: MainNavigationEventSink? = null
|
||||
|
||||
/** The avatar view owns the shared element transition out of this screen. */
|
||||
private var avatarView: View? = null
|
||||
@@ -130,7 +131,7 @@ class ConversationSettingsFragment : ComposeFragment() {
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
transitionCallback = context as? TransitionCallback
|
||||
chatRouter = context as? MainNavigationChatDetailRouter
|
||||
mainNavigationEventSink = context as? MainNavigationEventSink
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -358,7 +359,7 @@ class ConversationSettingsFragment : ComposeFragment() {
|
||||
val builder = ConversationIntents.createBuilderSync(requireContext(), action.recipientId, action.threadId)
|
||||
startActivity(builder.withSearchOpen(action.withSearchOpen).build())
|
||||
|
||||
if (action.withSearchOpen && requireActivity() !is MainNavigationChatDetailRouter) {
|
||||
if (action.withSearchOpen && requireActivity() !is MainNavigationEventSink) {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
@@ -453,7 +454,7 @@ class ConversationSettingsFragment : ComposeFragment() {
|
||||
}
|
||||
is ConversationSettingsAction.OpenGroupConversation -> {
|
||||
CommunicationActions.startConversation(requireActivity(), action.recipient, null)
|
||||
if (requireActivity() !is MainNavigationChatDetailRouter) {
|
||||
if (requireActivity() !is MainNavigationEventSink) {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
@@ -558,8 +559,9 @@ class ConversationSettingsFragment : ComposeFragment() {
|
||||
}
|
||||
|
||||
private fun goToConversationList() {
|
||||
if (chatRouter != null) {
|
||||
chatRouter?.exitDetailLocation()
|
||||
val eventSink = mainNavigationEventSink
|
||||
if (eventSink != null) {
|
||||
eventSink.onEvent(MainNavigationEvents.ExitDetail)
|
||||
} else {
|
||||
startActivity(MainActivity.clearTopAndExitDetail(requireContext()))
|
||||
}
|
||||
|
||||
+4
-3
@@ -7,7 +7,8 @@ package org.thoughtcrime.securesms.components.settings.conversation
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import org.thoughtcrime.securesms.main.MainDetailRoute
|
||||
import org.thoughtcrime.securesms.main.MainNavigationChatDetailRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEventSink
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEvents
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
||||
/**
|
||||
@@ -19,8 +20,8 @@ object ConversationSettingsNavigator {
|
||||
activity: FragmentActivity,
|
||||
recipient: Recipient
|
||||
) {
|
||||
if (activity is MainNavigationChatDetailRouter) {
|
||||
activity.goToChatDetail(MainDetailRoute.Chats.ConversationSettings(recipient.id))
|
||||
if (activity is MainNavigationEventSink) {
|
||||
activity.onEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.Chats.ConversationSettings(recipient.id)))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+25
-4
@@ -28,7 +28,8 @@ import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.jobs.ConversationShortcutUpdateJob
|
||||
import org.thoughtcrime.securesms.main.MainDetailRoute
|
||||
import org.thoughtcrime.securesms.main.MainNavigationChatDetailRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEventSink
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEvents
|
||||
import org.thoughtcrime.securesms.messagedetails.MessageDetailsFragment
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -36,7 +37,7 @@ import java.util.concurrent.TimeUnit
|
||||
/**
|
||||
* Wrapper activity for ConversationFragment.
|
||||
*/
|
||||
open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, GooglePayComponent, MainNavigationChatDetailRouter {
|
||||
open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, GooglePayComponent, MainNavigationEventSink {
|
||||
|
||||
companion object {
|
||||
private val TAG = tag(ConversationActivity::class.java)
|
||||
@@ -141,13 +142,33 @@ open class ConversationActivity : PassphraseRequiredActivity(), VoiceNoteMediaCo
|
||||
.commitNowAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun exitDetailLocation() {
|
||||
/**
|
||||
* Only the events a conversation displayed on its own can answer. The rest belong to the main window,
|
||||
* which is where the same screens send them when it is the one hosting them.
|
||||
*/
|
||||
override fun onEvent(event: MainNavigationEvents) {
|
||||
when (event) {
|
||||
MainNavigationEvents.ExitDetail -> exitDetail()
|
||||
is MainNavigationEvents.GoToDetail -> {
|
||||
val route = event.route
|
||||
if (route is MainDetailRoute.Chats) {
|
||||
goToChatDetail(route)
|
||||
} else {
|
||||
Log.w(TAG, "Cannot display $route outside of the main window. Ignoring it.")
|
||||
}
|
||||
}
|
||||
|
||||
else -> Log.w(TAG, "Unsupported outside of the main window: $event. Ignoring it.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun exitDetail() {
|
||||
if (!supportFragmentManager.popBackStackImmediate()) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun goToChatDetail(location: MainDetailRoute.Chats) {
|
||||
private fun goToChatDetail(location: MainDetailRoute.Chats) {
|
||||
when (location) {
|
||||
is MainDetailRoute.Chats.ConversationSettings -> {
|
||||
lifecycleScope.launch {
|
||||
|
||||
+10
-8
@@ -298,7 +298,8 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModelV2
|
||||
import org.thoughtcrime.securesms.longmessage.LongMessageFragment
|
||||
import org.thoughtcrime.securesms.main.MainDetailRoute
|
||||
import org.thoughtcrime.securesms.main.MainListRoute
|
||||
import org.thoughtcrime.securesms.main.MainNavigationChatDetailRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEventSink
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEvents
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel
|
||||
import org.thoughtcrime.securesms.main.MainSnackbarHostKey
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
|
||||
@@ -581,7 +582,7 @@ class ConversationFragment :
|
||||
private lateinit var conversationItemDecorations: ConversationItemDecorations
|
||||
private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback
|
||||
|
||||
private lateinit var chatRouter: MainNavigationChatDetailRouter
|
||||
private lateinit var mainNavigationEventSink: MainNavigationEventSink
|
||||
|
||||
private var animationsAllowed = false
|
||||
private var pinnedShortcutReceiver: BroadcastReceiver? = null
|
||||
@@ -662,7 +663,7 @@ class ConversationFragment :
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
chatRouter = context as MainNavigationChatDetailRouter
|
||||
mainNavigationEventSink = context as MainNavigationEventSink
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -3004,7 +3005,7 @@ class ConversationFragment :
|
||||
ConversationDialogs.displayDeleteDialog(requireContext(), recipient) {
|
||||
messageRequestViewModel
|
||||
.onDelete()
|
||||
.doAfterSuccess { chatRouter.exitDetailLocation() }
|
||||
.doAfterSuccess { mainNavigationEventSink.onEvent(MainNavigationEvents.ExitDetail) }
|
||||
.subscribeWithShowProgress("delete message request")
|
||||
}
|
||||
}
|
||||
@@ -3151,7 +3152,8 @@ class ConversationFragment :
|
||||
|
||||
private fun handleDisplayDetails(conversationMessage: ConversationMessage) {
|
||||
val recipientSnapshot = viewModel.recipientSnapshot ?: return
|
||||
chatRouter.goToChatDetail(MainDetailRoute.Chats.MessageDetails(recipientSnapshot.id, MessageId(conversationMessage.messageRecord.id)))
|
||||
val messageDetails = MainDetailRoute.Chats.MessageDetails(recipientSnapshot.id, MessageId(conversationMessage.messageRecord.id))
|
||||
mainNavigationEventSink.onEvent(MainNavigationEvents.GoToDetail(messageDetails))
|
||||
}
|
||||
|
||||
private fun handleDeleteMessages(messageParts: Set<MultiselectPart>) {
|
||||
@@ -3707,7 +3709,7 @@ class ConversationFragment :
|
||||
} else if (messageRecord.hasFailedWithNetworkFailures()) {
|
||||
ConversationDialogs.displayMessageCouldNotBeSentDialog(requireContext(), messageRecord)
|
||||
} else {
|
||||
chatRouter.goToChatDetail(MainDetailRoute.Chats.MessageDetails(recipientId, MessageId(messageRecord.id)))
|
||||
mainNavigationEventSink.onEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.Chats.MessageDetails(recipientId, MessageId(messageRecord.id))))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4388,7 +4390,7 @@ class ConversationFragment :
|
||||
override fun handleManageGroup() {
|
||||
viewModel.recipientSnapshot?.let { recipient ->
|
||||
container.hideKeyboard(composeText)
|
||||
chatRouter.goToChatDetail(MainDetailRoute.Chats.ConversationSettings(recipient.id))
|
||||
mainNavigationEventSink.onEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.Chats.ConversationSettings(recipient.id)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4426,7 +4428,7 @@ class ConversationFragment :
|
||||
viewModel.recipientSnapshot?.let { recipient ->
|
||||
if (!viewModel.hasMessageRequestState || recipient.isBlocked) {
|
||||
container.hideKeyboard(composeText)
|
||||
chatRouter.goToChatDetail(MainDetailRoute.Chats.ConversationSettings(recipient.id))
|
||||
mainNavigationEventSink.onEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.Chats.ConversationSettings(recipient.id)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.snackbars.SnackbarState;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.main.MainListRoute;
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEvents;
|
||||
import org.thoughtcrime.securesms.main.MainSnackbarHostKey;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
|
||||
@@ -77,7 +78,7 @@ public class ConversationListArchiveFragment extends ConversationListFragment
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
mainNavigationViewModel.goTo(MainListRoute.Chats);
|
||||
mainNavigationViewModel.onEvent(new MainNavigationEvents.GoToList(MainListRoute.Chats));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+3
-2
@@ -144,6 +144,7 @@ import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.AccountValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.main.MainListRoute;
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEvents;
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel;
|
||||
import org.thoughtcrime.securesms.main.MainSnackbarHostKey;
|
||||
import org.thoughtcrime.securesms.main.MainToolbarMode;
|
||||
@@ -702,7 +703,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
@Override
|
||||
public void onShowArchiveClick() {
|
||||
if (viewModel.currentSelectedConversations().isEmpty()) {
|
||||
mainNavigationViewModel.goTo(MainListRoute.Archive);
|
||||
mainNavigationViewModel.onEvent(new MainNavigationEvents.GoToList(MainListRoute.Archive));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,7 +766,7 @@ public class ConversationListFragment extends MainFragment implements Conversati
|
||||
} else if (event instanceof MainToolbarViewModel.Event.Chats.ClearFilter) {
|
||||
onClearFilterClick();
|
||||
} else if (event instanceof MainToolbarViewModel.Event.Chats.CloseArchive) {
|
||||
mainNavigationViewModel.goTo(MainListRoute.Chats);
|
||||
mainNavigationViewModel.onEvent(new MainNavigationEvents.GoToList(MainListRoute.Chats));
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.main
|
||||
|
||||
/**
|
||||
* An activity that hosts main navigation content and can answer its events.
|
||||
*
|
||||
* The main window hands them to [MainNavigationViewModel]. The standalone conversation activity answers
|
||||
* the few that still mean something outside the main window with its own fragment transactions. Screens
|
||||
* that either one can host send their events here, and check for it to tell the two hosts apart.
|
||||
*/
|
||||
fun interface MainNavigationEventSink {
|
||||
fun onEvent(event: MainNavigationEvents)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.main
|
||||
|
||||
import org.signal.core.ui.compose.split.ListDetailEvents
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphone
|
||||
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||
|
||||
/**
|
||||
* UI Events to drive the main navigation view model.
|
||||
*/
|
||||
sealed interface MainNavigationEvents {
|
||||
|
||||
/**
|
||||
* The user clicked [tab] in the navigation bar or rail. Clicking the tab already displayed reveals its
|
||||
* list and tells that tab's screen about the click, rather than navigating anywhere.
|
||||
*/
|
||||
data class GoToTab(val tab: MainListRoute) : MainNavigationEvents
|
||||
|
||||
/** Display [route], which comes back to whatever detail content its tab had open. */
|
||||
data class GoToList(val route: MainListRoute) : MainNavigationEvents
|
||||
|
||||
/** Open [route] above the list of whichever tab owns it. */
|
||||
data class GoToDetail(val route: MainDetailRoute) : MainNavigationEvents
|
||||
|
||||
/** Drop the detail above the displayed list, leaving that list on its own. */
|
||||
data object ExitDetail : MainNavigationEvents
|
||||
|
||||
/** Open the camera straight into story capture. */
|
||||
data object GoToCameraFirstStoryCapture : MainNavigationEvents
|
||||
|
||||
/** Re-read the settings that shape the navigation bar. */
|
||||
data object RefreshNavigationBar : MainNavigationEvents
|
||||
|
||||
/** Ask for the next megaphone to display, if there is one. */
|
||||
data object RequestNextMegaphone : MainNavigationEvents
|
||||
|
||||
/** [megaphone] was displayed to the user. */
|
||||
data class MegaphoneVisible(val megaphone: Megaphone) : MainNavigationEvents
|
||||
|
||||
/** The user put [event]'s megaphone off until later. */
|
||||
data class MegaphoneSnoozed(val event: Megaphones.Event) : MainNavigationEvents
|
||||
|
||||
/** [event]'s megaphone is done with, and should not come back. */
|
||||
data class MegaphoneCompleted(val event: Megaphones.Event) : MainNavigationEvents
|
||||
|
||||
/** Something that is navigation and nothing else, so the navigator can answer it unaided. */
|
||||
data class ListDetailEvent(val event: ListDetailEvents) : MainNavigationEvents
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.main
|
||||
|
||||
/**
|
||||
* Handles navigation for sub-screens within the chats detail pane.
|
||||
*/
|
||||
interface MainNavigationChatDetailRouter {
|
||||
fun exitDetailLocation()
|
||||
fun goToChatDetail(location: MainDetailRoute.Chats)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles navigation for sub-screens within the calls detail pane.
|
||||
*/
|
||||
interface MainNavigationCallDetailRouter {
|
||||
fun exitDetailLocation()
|
||||
fun goToCallDetail(location: MainDetailRoute.Calls)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles navigation to all [MainListRoute]s and [MainDetailRoute]s, including the top-level roots.
|
||||
*/
|
||||
interface MainNavigationRouter : MainNavigationChatDetailRouter, MainNavigationCallDetailRouter {
|
||||
fun goTo(location: MainListRoute)
|
||||
fun goTo(location: MainDetailRoute)
|
||||
|
||||
override fun goToChatDetail(location: MainDetailRoute.Chats) = goTo(location)
|
||||
override fun goToCallDetail(location: MainDetailRoute.Calls) = goTo(location)
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import kotlinx.coroutines.reactive.asFlow
|
||||
import kotlinx.coroutines.rx3.asObservable
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.signal.core.ui.compose.EventDrivenViewModel
|
||||
import org.signal.core.ui.compose.split.ListDetailEvents
|
||||
import org.signal.core.ui.compose.split.ListDetailNavigator
|
||||
import org.signal.core.ui.compose.split.PaneAnchor
|
||||
@@ -49,7 +50,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
||||
class MainNavigationViewModel(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
initialListLocation: MainListRoute = MainListRoute.Chats
|
||||
) : ViewModel(), MainNavigationRouter {
|
||||
) : EventDrivenViewModel<MainNavigationEvents>(TAG), MainNavigationEventSink {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(MainNavigationViewModel::class)
|
||||
@@ -156,11 +157,20 @@ class MainNavigationViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The user dragged the pane divider to [anchor].
|
||||
*/
|
||||
fun onPaneAnchorSelected(anchor: PaneAnchor) {
|
||||
navigator.processEvent(ListDetailEvents.AnchorSelected(anchor))
|
||||
override suspend fun processEvent(event: MainNavigationEvents) {
|
||||
when (event) {
|
||||
is MainNavigationEvents.GoToTab -> goToTab(event.tab)
|
||||
is MainNavigationEvents.GoToList -> goToList(event.route)
|
||||
is MainNavigationEvents.GoToDetail -> goToDetail(event.route)
|
||||
MainNavigationEvents.ExitDetail -> navigator.processEvent(ListDetailEvents.ExitDetail)
|
||||
MainNavigationEvents.GoToCameraFirstStoryCapture -> internalNavigationEvents.emit(NavigationEvent.STORY_CAMERA_FIRST)
|
||||
MainNavigationEvents.RefreshNavigationBar -> refreshNavigationBarState()
|
||||
MainNavigationEvents.RequestNextMegaphone -> requestNextMegaphone()
|
||||
is MainNavigationEvents.MegaphoneVisible -> megaphoneRepository.markVisible(event.megaphone.event)
|
||||
is MainNavigationEvents.MegaphoneSnoozed -> onMegaphoneSnoozed(event.event)
|
||||
is MainNavigationEvents.MegaphoneCompleted -> onMegaphoneCompleted(event.event)
|
||||
is MainNavigationEvents.ListDetailEvent -> navigator.processEvent(event.event)
|
||||
}
|
||||
}
|
||||
|
||||
/** Set from the MainActivity composition, and cleared when it is disposed. */
|
||||
@@ -168,7 +178,38 @@ class MainNavigationViewModel(
|
||||
captureChatListSnapshot = capture
|
||||
}
|
||||
|
||||
override fun goTo(location: MainDetailRoute) = setDetailLocation(location)
|
||||
/**
|
||||
* Clicking the tab already displayed reveals its list and tells that tab's screen about the click —
|
||||
* scrolling to the top, and the like — rather than navigating anywhere.
|
||||
*/
|
||||
private suspend fun goToTab(tab: MainListRoute) {
|
||||
if (navigator.displayedList.value == tab) {
|
||||
navigator.processEvent(ListDetailEvents.RevealList)
|
||||
internalTabClickEvents.emit(tab)
|
||||
} else {
|
||||
goToList(tab)
|
||||
}
|
||||
}
|
||||
|
||||
/** Switching tabs only changes which stack is displayed; each tab comes back to whatever it had open. */
|
||||
private fun goToList(route: MainListRoute) {
|
||||
navigator.processEvent(
|
||||
ListDetailEvents.GoToList(
|
||||
listRoute = route,
|
||||
root = route.tab,
|
||||
push = route == MainListRoute.Archive
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun goToDetail(route: MainDetailRoute) {
|
||||
when (route) {
|
||||
is MainDetailRoute.Chats -> pushChatsDetailLocation(route)
|
||||
is MainDetailRoute.Conversation -> goToConversation(route)
|
||||
is MainDetailRoute.Calls, is MainDetailRoute.CallLinkDetails -> pushCallsDetailLocation(route)
|
||||
is MainDetailRoute.Stories -> navigator.processEvent(ListDetailEvents.Push(route, MainListRoute.Stories))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MainDetailRoute.Conversation.withPreloadedWallpaper(): MainDetailRoute.Conversation {
|
||||
val args = conversationArgs
|
||||
@@ -196,33 +237,15 @@ class MainNavigationViewModel(
|
||||
return copy(conversationArgs = updatedArgs)
|
||||
}
|
||||
|
||||
private fun setDetailLocation(location: MainDetailRoute) {
|
||||
when (location) {
|
||||
is MainDetailRoute.Chats -> pushChatsDetailLocation(location)
|
||||
is MainDetailRoute.Conversation -> goToConversation(location)
|
||||
is MainDetailRoute.Calls, is MainDetailRoute.CallLinkDetails -> pushCallsDetailLocation(location)
|
||||
is MainDetailRoute.Stories -> pushStoriesDetailLocation(location)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the detail content above the current list, leaving that list displayed on its own.
|
||||
*/
|
||||
override fun exitDetailLocation() {
|
||||
navigator.processEvent(ListDetailEvents.ExitDetail)
|
||||
}
|
||||
|
||||
private fun goToConversation(location: MainDetailRoute.Conversation) {
|
||||
private suspend fun goToConversation(location: MainDetailRoute.Conversation) {
|
||||
val captureSnapshot = captureChatListSnapshot
|
||||
|
||||
if (captureSnapshot == null) {
|
||||
// share intent or process restore - push synchronously, since there's no chat-list snapshot to capture and no need to preload a wallpaper
|
||||
// share intent or process restore - there's no chat-list snapshot to capture and no need to preload a wallpaper
|
||||
pushChatsDetailLocation(location)
|
||||
} else {
|
||||
viewModelScope.launch {
|
||||
captureSnapshot()
|
||||
pushChatsDetailLocation(location.withPreloadedWallpaper())
|
||||
}
|
||||
captureSnapshot()
|
||||
pushChatsDetailLocation(location.withPreloadedWallpaper())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,53 +267,23 @@ class MainNavigationViewModel(
|
||||
navigator.processEvent(ListDetailEvents.Push(location, MainListRoute.Calls))
|
||||
}
|
||||
|
||||
private fun pushStoriesDetailLocation(location: MainDetailRoute) {
|
||||
navigator.processEvent(ListDetailEvents.Push(location, MainListRoute.Stories))
|
||||
}
|
||||
|
||||
/** Pops the stack belonging to whichever tab the user is currently on. */
|
||||
fun popCurrentDetailLocation() {
|
||||
navigator.processEvent(ListDetailEvents.Back)
|
||||
}
|
||||
|
||||
/** Switching tabs only changes which stack is displayed; each tab comes back to whatever it had open. */
|
||||
override fun goTo(location: MainListRoute) {
|
||||
navigator.processEvent(
|
||||
ListDetailEvents.GoToList(
|
||||
listRoute = location,
|
||||
root = location.tab,
|
||||
push = location == MainListRoute.Archive
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun goToCameraFirstStoryCapture() {
|
||||
viewModelScope.launch {
|
||||
internalNavigationEvents.emit(NavigationEvent.STORY_CAMERA_FIRST)
|
||||
}
|
||||
}
|
||||
|
||||
fun getNextMegaphone() {
|
||||
private fun requestNextMegaphone() {
|
||||
megaphoneRepository.getNextMegaphone { next ->
|
||||
internalMegaphone.update { next ?: Megaphone.NONE }
|
||||
}
|
||||
}
|
||||
|
||||
fun onMegaphoneSnoozed(event: Megaphones.Event) {
|
||||
private fun onMegaphoneSnoozed(event: Megaphones.Event) {
|
||||
megaphoneRepository.markInteractedWith(event)
|
||||
internalMegaphone.update { Megaphone.NONE }
|
||||
}
|
||||
|
||||
fun onMegaphoneCompleted(event: Megaphones.Event) {
|
||||
private fun onMegaphoneCompleted(event: Megaphones.Event) {
|
||||
internalMegaphone.update { Megaphone.NONE }
|
||||
megaphoneRepository.markFinished(event)
|
||||
}
|
||||
|
||||
fun onMegaphoneVisible(visible: Megaphone) {
|
||||
megaphoneRepository.markVisible(visible.event)
|
||||
}
|
||||
|
||||
fun refreshNavigationBarState() {
|
||||
private fun refreshNavigationBarState() {
|
||||
internalMainNavigationBarState.update {
|
||||
it.copy(
|
||||
compact = SignalStore.settings.useCompactNavigationBar,
|
||||
@@ -305,34 +298,6 @@ class MainNavigationViewModel(
|
||||
return notificationProfilesRepository.getProfiles().asFlow()
|
||||
}
|
||||
|
||||
fun onChatsSelected() {
|
||||
onTabSelected(MainListRoute.Chats)
|
||||
}
|
||||
|
||||
fun onArchiveSelected() {
|
||||
onTabSelected(MainListRoute.Archive)
|
||||
}
|
||||
|
||||
fun onCallsSelected() {
|
||||
onTabSelected(MainListRoute.Calls)
|
||||
}
|
||||
|
||||
fun onStoriesSelected() {
|
||||
onTabSelected(MainListRoute.Stories)
|
||||
}
|
||||
|
||||
private fun onTabSelected(destination: MainListRoute) {
|
||||
viewModelScope.launch {
|
||||
val displayed = navigator.displayedList.value
|
||||
if (displayed == destination) {
|
||||
navigator.processEvent(ListDetailEvents.RevealList)
|
||||
internalTabClickEvents.emit(destination)
|
||||
} else {
|
||||
goTo(destination)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> performStoreUpdate(flow: Flow<T>, fn: (T, MainNavigationBarState) -> MainNavigationBarState) {
|
||||
viewModelScope.launch {
|
||||
flow.collectLatest { item ->
|
||||
|
||||
+4
-3
@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.main.MainDetailRoute
|
||||
import org.thoughtcrime.securesms.main.MainListRoute
|
||||
import org.thoughtcrime.securesms.main.MainNavigationEvents
|
||||
import org.thoughtcrime.securesms.main.MainNavigationViewModel
|
||||
import org.thoughtcrime.securesms.main.MainSnackbarHostKey
|
||||
import org.thoughtcrime.securesms.main.MainToolbarViewModel
|
||||
@@ -188,7 +189,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
MyStoriesItem.Model(
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
onClick = {
|
||||
mainNavigationViewModel.goToCameraFirstStoryCapture()
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToCameraFirstStoryCapture)
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -258,7 +259,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
openStoryViewer(model, preview, true)
|
||||
},
|
||||
onAvatarClick = {
|
||||
mainNavigationViewModel.goToCameraFirstStoryCapture()
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToCameraFirstStoryCapture)
|
||||
},
|
||||
onLockList = {
|
||||
recyclerView?.suppressLayout(true)
|
||||
@@ -271,7 +272,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
|
||||
|
||||
private fun openStoryViewer(model: StoriesLandingItem.Model, preview: View, isFromInfoContextMenuAction: Boolean) {
|
||||
if (model.data.storyRecipient.isMyStory) {
|
||||
mainNavigationViewModel.goTo(MainDetailRoute.Stories.MyStories)
|
||||
mainNavigationViewModel.onEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.Stories.MyStories))
|
||||
} else if (model.data.primaryStory.messageRecord.isOutgoing && model.data.primaryStory.messageRecord.isFailed) {
|
||||
if (model.data.primaryStory.messageRecord.isIdentityMismatchFailure) {
|
||||
SafetyNumberBottomSheet
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.signal.core.ui.compose.split.ListDetailEvents
|
||||
import org.signal.core.ui.compose.split.PaneAnchor
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
@@ -33,9 +34,8 @@ import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
|
||||
|
||||
/**
|
||||
* Covers the view model as the sole owner of the tab back stacks: everything a screen can ask for goes
|
||||
* through [MainNavigationRouter] or [MainNavigationViewModel.popCurrentDetailLocation], and the stacks it
|
||||
* hands the display are what those calls leave behind.
|
||||
* Covers the view model as the sole owner of the tab back stacks: everything a screen can ask for arrives
|
||||
* as a [MainNavigationEvents], and the stacks it hands the display are what those events leave behind.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@@ -79,6 +79,12 @@ class MainNavigationViewModelTest {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
/** Events are answered off the view model's own channel, so every one of them is drained before asserting. */
|
||||
private fun MainNavigationViewModel.sendEvent(event: MainNavigationEvents) {
|
||||
onEvent(event)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a new view model, then chats is displayed with no detail`() {
|
||||
assertEquals(MainListRoute.Chats, viewModel.currentTab.value)
|
||||
@@ -87,41 +93,41 @@ class MainNavigationViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `when going to a detail location, then it is pushed onto the displayed tab`() {
|
||||
viewModel.goTo(conversationSettings)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(conversationSettings))
|
||||
|
||||
assertEquals(listOf(MainListRoute.Chats, conversationSettings), viewModel.navigator[MainListRoute.Chats])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given stacked detail, when popping, then only the top of the stack is dropped`() {
|
||||
viewModel.goTo(conversationSettings)
|
||||
viewModel.goTo(messageDetails)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(conversationSettings))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(messageDetails))
|
||||
|
||||
viewModel.popCurrentDetailLocation()
|
||||
viewModel.sendEvent(MainNavigationEvents.ListDetailEvent(ListDetailEvents.Back))
|
||||
|
||||
assertEquals(listOf(MainListRoute.Chats, conversationSettings), viewModel.navigator[MainListRoute.Chats])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given stacked detail, when exiting detail, then all of it is dropped`() {
|
||||
viewModel.goTo(conversationSettings)
|
||||
viewModel.goTo(messageDetails)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(conversationSettings))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(messageDetails))
|
||||
|
||||
viewModel.exitDetailLocation()
|
||||
viewModel.sendEvent(MainNavigationEvents.ExitDetail)
|
||||
|
||||
assertEquals(listOf(MainListRoute.Chats), viewModel.navigator[MainListRoute.Chats])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a stack at its root, when popping, then nothing is dropped`() {
|
||||
viewModel.popCurrentDetailLocation()
|
||||
viewModel.sendEvent(MainNavigationEvents.ListDetailEvent(ListDetailEvents.Back))
|
||||
|
||||
assertEquals(listOf(MainListRoute.Chats), viewModel.navigator[MainListRoute.Chats])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when going to the archive, then it is pushed onto the chats stack rather than becoming a tab`() {
|
||||
viewModel.goTo(MainListRoute.Archive)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Archive))
|
||||
|
||||
assertEquals(MainListRoute.Chats, viewModel.currentTab.value)
|
||||
assertEquals(
|
||||
@@ -132,9 +138,9 @@ class MainNavigationViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `given the archive is displayed, when going back to chats, then the archive is popped`() {
|
||||
viewModel.goTo(MainListRoute.Archive)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Archive))
|
||||
|
||||
viewModel.goTo(MainListRoute.Chats)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Chats))
|
||||
|
||||
assertEquals(listOf(MainListRoute.Chats), viewModel.navigator[MainListRoute.Chats])
|
||||
}
|
||||
@@ -145,10 +151,10 @@ class MainNavigationViewModelTest {
|
||||
*/
|
||||
@Test
|
||||
fun `given detail opened from the archive, when exiting detail, then the archive stays displayed`() {
|
||||
viewModel.goTo(MainListRoute.Archive)
|
||||
viewModel.goTo(conversationSettings)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Archive))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(conversationSettings))
|
||||
|
||||
viewModel.exitDetailLocation()
|
||||
viewModel.sendEvent(MainNavigationEvents.ExitDetail)
|
||||
|
||||
assertEquals(
|
||||
listOf(MainListRoute.Chats, MainListRoute.Archive),
|
||||
@@ -158,9 +164,9 @@ class MainNavigationViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `given detail is open, when opening the archive, then the detail stays displayed above it`() {
|
||||
viewModel.goTo(conversationSettings)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(conversationSettings))
|
||||
|
||||
viewModel.goTo(MainListRoute.Archive)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Archive))
|
||||
|
||||
assertEquals(
|
||||
listOf(MainListRoute.Chats, MainListRoute.Archive, conversationSettings),
|
||||
@@ -170,8 +176,8 @@ class MainNavigationViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `when going to a calls destination, then it is pushed onto the calls stack`() {
|
||||
viewModel.goTo(MainListRoute.Calls)
|
||||
viewModel.goTo(callLinkDetails)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Calls))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(callLinkDetails))
|
||||
|
||||
assertEquals(MainListRoute.Calls, viewModel.currentTab.value)
|
||||
assertEquals(listOf(MainListRoute.Calls, callLinkDetails), viewModel.navigator[MainListRoute.Calls])
|
||||
@@ -183,11 +189,11 @@ class MainNavigationViewModelTest {
|
||||
*/
|
||||
@Test
|
||||
fun `given detail open on another tab, when switching away and back, then that stack is unchanged`() {
|
||||
viewModel.goTo(MainListRoute.Calls)
|
||||
viewModel.goTo(callLinkDetails)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Calls))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(callLinkDetails))
|
||||
|
||||
viewModel.goTo(MainListRoute.Chats)
|
||||
viewModel.goTo(MainListRoute.Calls)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Chats))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Calls))
|
||||
|
||||
assertEquals(listOf(MainListRoute.Calls, callLinkDetails), viewModel.navigator[MainListRoute.Calls])
|
||||
}
|
||||
@@ -198,12 +204,12 @@ class MainNavigationViewModelTest {
|
||||
*/
|
||||
@Test
|
||||
fun `given detail open on both tabs, when popping from calls, then only the calls stack is affected`() {
|
||||
viewModel.goTo(MainListRoute.Chats)
|
||||
viewModel.goTo(conversationSettings)
|
||||
viewModel.goTo(MainListRoute.Calls)
|
||||
viewModel.goTo(callLinkDetails)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Chats))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(conversationSettings))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Calls))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(callLinkDetails))
|
||||
|
||||
viewModel.popCurrentDetailLocation()
|
||||
viewModel.sendEvent(MainNavigationEvents.ListDetailEvent(ListDetailEvents.Back))
|
||||
|
||||
assertEquals(listOf(MainListRoute.Calls), viewModel.navigator[MainListRoute.Calls])
|
||||
assertEquals(listOf(MainListRoute.Chats, conversationSettings), viewModel.navigator[MainListRoute.Chats])
|
||||
@@ -211,12 +217,12 @@ class MainNavigationViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `given detail open on both tabs, when exiting detail from calls, then only the calls stack is affected`() {
|
||||
viewModel.goTo(MainListRoute.Chats)
|
||||
viewModel.goTo(conversationSettings)
|
||||
viewModel.goTo(MainListRoute.Calls)
|
||||
viewModel.goTo(callLinkDetails)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Chats))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(conversationSettings))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Calls))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(callLinkDetails))
|
||||
|
||||
viewModel.exitDetailLocation()
|
||||
viewModel.sendEvent(MainNavigationEvents.ExitDetail)
|
||||
|
||||
assertEquals(listOf(MainListRoute.Calls), viewModel.navigator[MainListRoute.Calls])
|
||||
assertEquals(listOf(MainListRoute.Chats, conversationSettings), viewModel.navigator[MainListRoute.Chats])
|
||||
@@ -224,12 +230,12 @@ class MainNavigationViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `given detail open on another tab, when popping from chats, then only the chats stack is affected`() {
|
||||
viewModel.goTo(MainListRoute.Calls)
|
||||
viewModel.goTo(callLinkDetails)
|
||||
viewModel.goTo(MainListRoute.Chats)
|
||||
viewModel.goTo(conversationSettings)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Calls))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(callLinkDetails))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Chats))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(conversationSettings))
|
||||
|
||||
viewModel.popCurrentDetailLocation()
|
||||
viewModel.sendEvent(MainNavigationEvents.ListDetailEvent(ListDetailEvents.Back))
|
||||
|
||||
assertEquals(listOf(MainListRoute.Chats), viewModel.navigator[MainListRoute.Chats])
|
||||
assertEquals(listOf(MainListRoute.Calls, callLinkDetails), viewModel.navigator[MainListRoute.Calls])
|
||||
@@ -237,8 +243,8 @@ class MainNavigationViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `when going to a stories destination, then it is pushed onto the stories stack`() {
|
||||
viewModel.goTo(MainListRoute.Stories)
|
||||
viewModel.goTo(MainDetailRoute.Stories.MyStories)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Stories))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.Stories.MyStories))
|
||||
|
||||
assertEquals(
|
||||
listOf(MainListRoute.Stories, MainDetailRoute.Stories.MyStories),
|
||||
@@ -252,10 +258,10 @@ class MainNavigationViewModelTest {
|
||||
*/
|
||||
@Test
|
||||
fun `given a stories destination is open, when opening another, then it replaces the first`() {
|
||||
viewModel.goTo(MainListRoute.Stories)
|
||||
viewModel.goTo(MainDetailRoute.Stories.MyStories)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToList(MainListRoute.Stories))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.Stories.MyStories))
|
||||
|
||||
viewModel.goTo(MainDetailRoute.Stories.PrivacySettings)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(MainDetailRoute.Stories.PrivacySettings))
|
||||
|
||||
assertEquals(
|
||||
listOf(MainListRoute.Stories, MainDetailRoute.Stories.PrivacySettings),
|
||||
@@ -265,30 +271,30 @@ class MainNavigationViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `given the list fills the window, when detail content opens, then the detail is revealed`() {
|
||||
viewModel.onPaneAnchorSelected(PaneAnchor.LIST_ONLY)
|
||||
viewModel.sendEvent(MainNavigationEvents.ListDetailEvent(ListDetailEvents.AnchorSelected(PaneAnchor.LIST_ONLY)))
|
||||
|
||||
viewModel.goTo(conversationSettings)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(conversationSettings))
|
||||
|
||||
assertEquals(PaneAnchor.DETAIL_ONLY, viewModel.paneAnchor.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given the detail fills the window, when the last detail is popped, then the list is revealed`() {
|
||||
viewModel.goTo(conversationSettings)
|
||||
viewModel.onPaneAnchorSelected(PaneAnchor.DETAIL_ONLY)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(conversationSettings))
|
||||
viewModel.sendEvent(MainNavigationEvents.ListDetailEvent(ListDetailEvents.AnchorSelected(PaneAnchor.DETAIL_ONLY)))
|
||||
|
||||
viewModel.popCurrentDetailLocation()
|
||||
viewModel.sendEvent(MainNavigationEvents.ListDetailEvent(ListDetailEvents.Back))
|
||||
|
||||
assertEquals(PaneAnchor.LIST_ONLY, viewModel.paneAnchor.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given stacked detail, when the top is popped, then the detail pane stays revealed`() {
|
||||
viewModel.goTo(conversationSettings)
|
||||
viewModel.goTo(messageDetails)
|
||||
viewModel.onPaneAnchorSelected(PaneAnchor.DETAIL_ONLY)
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(conversationSettings))
|
||||
viewModel.sendEvent(MainNavigationEvents.GoToDetail(messageDetails))
|
||||
viewModel.sendEvent(MainNavigationEvents.ListDetailEvent(ListDetailEvents.AnchorSelected(PaneAnchor.DETAIL_ONLY)))
|
||||
|
||||
viewModel.popCurrentDetailLocation()
|
||||
viewModel.sendEvent(MainNavigationEvents.ListDetailEvent(ListDetailEvents.Back))
|
||||
|
||||
assertEquals(PaneAnchor.DETAIL_ONLY, viewModel.paneAnchor.value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user