mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-06 05:14:50 +01:00
Migrate calls tab to nav 3.
This commit is contained in:
committed by
Michelle Tang
parent
7505ef1834
commit
5ed1bdc26a
@@ -70,7 +70,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.createSavedStateHandle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
@@ -101,6 +103,7 @@ import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgressState
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.CouldNotCompleteBackupRestoreSheet
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.verify.VerifyBackupKeyActivity
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
|
||||
import org.thoughtcrime.securesms.calls.callsNavEntries
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFilter
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogFragment
|
||||
import org.thoughtcrime.securesms.calls.new.NewCallActivity
|
||||
@@ -157,7 +160,6 @@ import org.thoughtcrime.securesms.main.MainToolbarMode
|
||||
import org.thoughtcrime.securesms.main.MainToolbarState
|
||||
import org.thoughtcrime.securesms.main.MainToolbarViewModel
|
||||
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
|
||||
import org.thoughtcrime.securesms.main.callNavGraphBuilder
|
||||
import org.thoughtcrime.securesms.main.navigateToDetailLocation
|
||||
import org.thoughtcrime.securesms.main.rememberDetailNavHostController
|
||||
import org.thoughtcrime.securesms.main.rememberFocusRequester
|
||||
@@ -507,15 +509,6 @@ class MainActivity :
|
||||
mainNavigationViewModel.onSplitPaneChanged(isSplitPane)
|
||||
}
|
||||
|
||||
val callsNavHostController = rememberDetailNavHostController(
|
||||
onRequestFocus = rememberFocusRequester(
|
||||
mainNavigationViewModel = mainNavigationViewModel,
|
||||
currentListLocation = mainNavigationState.currentListLocation
|
||||
) { it == MainNavigationListLocation.CALLS }
|
||||
) {
|
||||
callNavGraphBuilder(it)
|
||||
}
|
||||
|
||||
val storiesNavHostController = rememberDetailNavHostController(
|
||||
onRequestFocus = rememberFocusRequester(
|
||||
mainNavigationViewModel = mainNavigationViewModel,
|
||||
@@ -534,17 +527,22 @@ class MainActivity :
|
||||
throw IllegalStateException("Navigation to ${mainNavigationState.currentListLocation} should be handled by ChatsBackStack.")
|
||||
}
|
||||
|
||||
MainNavigationListLocation.CALLS -> callsNavHostController
|
||||
MainNavigationListLocation.STORIES -> storiesNavHostController
|
||||
}.navigateToDetailLocation(location)
|
||||
MainNavigationListLocation.CALLS -> {
|
||||
throw IllegalStateException("Navigation to ${MainNavigationListLocation.CALLS} should be handled by CallsBackStack.")
|
||||
}
|
||||
|
||||
MainNavigationListLocation.STORIES -> storiesNavHostController.navigateToDetailLocation(location)
|
||||
}
|
||||
}
|
||||
|
||||
is MainNavigationDetailLocation.Conversation, is MainNavigationDetailLocation.Chats -> {
|
||||
throw IllegalStateException("Navigation to $location should be handled by ChatsBackStack.")
|
||||
}
|
||||
|
||||
is MainNavigationDetailLocation.CallLinkDetails -> callsNavHostController.navigateToDetailLocation(location)
|
||||
is MainNavigationDetailLocation.Calls -> callsNavHostController.navigateToDetailLocation(location)
|
||||
is MainNavigationDetailLocation.CallLinkDetails, is MainNavigationDetailLocation.Calls -> {
|
||||
throw IllegalStateException("Navigation to $location should be handled by CallsBackStack.")
|
||||
}
|
||||
|
||||
is MainNavigationDetailLocation.Stories -> storiesNavHostController.navigateToDetailLocation(location)
|
||||
}
|
||||
}
|
||||
@@ -739,9 +737,17 @@ class MainActivity :
|
||||
}
|
||||
|
||||
MainNavigationListLocation.CALLS -> {
|
||||
DetailsScreenNavHost(
|
||||
navHostController = callsNavHostController,
|
||||
contentLayoutData = contentLayoutData
|
||||
NavDisplay(
|
||||
backStack = mainNavigationViewModel.callsBackStackEntries,
|
||||
onBack = { mainNavigationViewModel.popCallsDetailLocation() },
|
||||
transitionSpec = TransitionSpecs.HorizontalSlide.transitionSpec,
|
||||
popTransitionSpec = TransitionSpecs.HorizontalSlide.popTransitionSpec,
|
||||
predictivePopTransitionSpec = TransitionSpecs.HorizontalSlide.predictivePopTransitionSpec,
|
||||
entryDecorators = listOf(
|
||||
rememberSaveableStateHolderNavEntryDecorator(),
|
||||
rememberViewModelStoreNavEntryDecorator()
|
||||
),
|
||||
entryProvider = entryProvider { callsNavEntries(isSplitPane) }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||
import androidx.lifecycle.viewmodel.compose.saveable
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogRow
|
||||
import org.thoughtcrime.securesms.main.MainDetailBackStack
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||
|
||||
/**
|
||||
* Controls the navigation stack used by the calls screen.
|
||||
*/
|
||||
@OptIn(SavedStateHandleSaveableApi::class)
|
||||
class CallsBackStack(savedStateHandle: SavedStateHandle) : MainDetailBackStack {
|
||||
|
||||
companion object {
|
||||
private const val KEY = "calls_back_stack"
|
||||
|
||||
val saver: Saver<SnapshotStateList<MainNavigationDetailLocation>, ArrayList<MainNavigationDetailLocation>> = Saver(
|
||||
save = { ArrayList(it) },
|
||||
restore = { mutableStateListOf(*it.toTypedArray()) }
|
||||
)
|
||||
}
|
||||
|
||||
override val entries: SnapshotStateList<MainNavigationDetailLocation> = savedStateHandle.saveable(
|
||||
key = KEY,
|
||||
saver = saver
|
||||
) {
|
||||
mutableStateListOf(MainNavigationDetailLocation.Empty)
|
||||
}
|
||||
|
||||
val activeCallId: CallLogRow.Id?
|
||||
get() = entries.asReversed().firstNotNullOfOrNull { location ->
|
||||
when (location) {
|
||||
is MainNavigationDetailLocation.Calls -> location.controllerKey
|
||||
is MainNavigationDetailLocation.CallLinkDetails -> location.controllerKey
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes an entry onto the stack.
|
||||
*/
|
||||
override fun push(location: MainNavigationDetailLocation) {
|
||||
when {
|
||||
location is MainNavigationDetailLocation.Empty || location == entries.lastOrNull() -> Unit
|
||||
|
||||
location.isContentRoot -> {
|
||||
entries.removeAll { it !is MainNavigationDetailLocation.Empty }
|
||||
entries.add(location)
|
||||
}
|
||||
|
||||
else -> entries.add(location)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import org.signal.core.ui.navigation.TransitionSpecs
|
||||
import org.thoughtcrime.securesms.MainNavigator
|
||||
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameScreen
|
||||
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsScreen
|
||||
import org.thoughtcrime.securesms.main.EmptyDetailScreen
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||
|
||||
fun EntryProviderScope<NavKey>.callsNavEntries(isSplitPane: Boolean) {
|
||||
entry<MainNavigationDetailLocation.Empty> {
|
||||
NoCallSelectedEntry()
|
||||
}
|
||||
|
||||
entry<MainNavigationDetailLocation.CallLinkDetails>(
|
||||
metadata = if (isSplitPane) TransitionSpecs.None.metadata else emptyMap()
|
||||
) { route ->
|
||||
CallLinkDetailsEntry(route)
|
||||
}
|
||||
|
||||
entry<MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName> { route ->
|
||||
EditCallLinkNameEntry(route)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoCallSelectedEntry() {
|
||||
EmptyDetailScreen()
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CallLinkDetailsEntry(route: MainNavigationDetailLocation.CallLinkDetails) {
|
||||
informNavigatorWeAreReady()
|
||||
|
||||
CallLinkDetailsScreen(roomId = route.callLinkRoomId)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EditCallLinkNameEntry(route: MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName) {
|
||||
informNavigatorWeAreReady()
|
||||
|
||||
EditCallLinkNameScreen(
|
||||
roomId = route.callLinkRoomId,
|
||||
initialName = route.currentName
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun informNavigatorWeAreReady() {
|
||||
val navigator = LocalActivity.current as? MainNavigator.NavigatorProvider
|
||||
LaunchedEffect(navigator) {
|
||||
navigator?.onFirstRender()
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -89,7 +89,8 @@ class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
|
||||
|
||||
@Composable
|
||||
fun EditCallLinkNameScreen(
|
||||
roomId: CallLinkRoomId
|
||||
roomId: CallLinkRoomId,
|
||||
initialName: String
|
||||
) {
|
||||
val viewModel: CallLinkDetailsViewModel = viewModel {
|
||||
CallLinkDetailsViewModel(roomId)
|
||||
@@ -99,7 +100,7 @@ fun EditCallLinkNameScreen(
|
||||
val lifecycleScope = LocalLifecycleOwner.current.lifecycleScope
|
||||
|
||||
EditCallLinkNameScreen(
|
||||
initialNameValue = viewModel.nameSnapshot,
|
||||
initialNameValue = initialName,
|
||||
onSaveClick = {
|
||||
lifecycleScope.launch {
|
||||
viewModel.setName(it)
|
||||
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.calls.links.details
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import org.signal.core.ui.compose.theme.SignalTheme
|
||||
import org.signal.core.util.getParcelableExtraCompat
|
||||
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameDialogFragment
|
||||
import org.thoughtcrime.securesms.main.MainNavigationCallDetailRouter
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.util.viewModel
|
||||
|
||||
class CallLinkDetailsActivity : FragmentActivity() {
|
||||
|
||||
companion object {
|
||||
private const val ARG_ROOM_ID = "room.id"
|
||||
|
||||
fun createIntent(context: Context, callLinkRoomId: CallLinkRoomId): Intent {
|
||||
return Intent(context, CallLinkDetailsActivity::class.java)
|
||||
.putExtra(ARG_ROOM_ID, callLinkRoomId)
|
||||
}
|
||||
}
|
||||
|
||||
private val roomId: CallLinkRoomId
|
||||
get() = intent.getParcelableExtraCompat(ARG_ROOM_ID, CallLinkRoomId::class.java)!!
|
||||
|
||||
private val viewModel: CallLinkDetailsViewModel by viewModel {
|
||||
CallLinkDetailsViewModel(roomId)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
SignalTheme {
|
||||
CallLinkDetailsScreen(
|
||||
roomId = roomId,
|
||||
viewModel = viewModel,
|
||||
router = remember { Router() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class Router : MainNavigationCallDetailRouter {
|
||||
override fun goToCallDetail(location: MainNavigationDetailLocation.Calls) {
|
||||
when (location) {
|
||||
is MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName -> {
|
||||
EditCallLinkNameDialogFragment().apply {
|
||||
arguments = bundleOf(EditCallLinkNameDialogFragment.ARG_NAME to viewModel.nameSnapshot)
|
||||
}.show(supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun exitDetailLocation() = finishAfterTransition()
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -113,7 +113,12 @@ class DefaultCallLinkDetailsCallback(
|
||||
}
|
||||
|
||||
override fun onEditNameClicked() {
|
||||
router.goToCallDetail(MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName(callLinkRoomId = viewModel.recipientSnapshot!!.requireCallLinkRoomId()))
|
||||
router.goToCallDetail(
|
||||
MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName(
|
||||
callLinkRoomId = viewModel.recipientSnapshot!!.requireCallLinkRoomId(),
|
||||
currentName = viewModel.nameSnapshot
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onShareClicked() {
|
||||
|
||||
-7
@@ -6,7 +6,6 @@
|
||||
package org.thoughtcrime.securesms.calls.links.details
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
@@ -194,10 +193,4 @@ class CallLinkDetailsViewModel(
|
||||
private fun toastCouldNotDeleteCallLink() {
|
||||
_state.update { it.copy(failureSnackbar = CallLinkDetailsState.FailureSnackbar.COULD_NOT_DELETE_CALL_LINK) }
|
||||
}
|
||||
|
||||
class Factory(private val callLinkRoomId: CallLinkRoomId) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return modelClass.cast(CallLinkDetailsViewModel(callLinkRoomId)) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@ import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
|
||||
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.database.CallTable
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.signal.core.ui.R as CoreUiR
|
||||
|
||||
@@ -122,11 +122,10 @@ class CallLogContextMenu(
|
||||
iconRes = CoreUiR.drawable.symbol_info_24,
|
||||
title = fragment.getString(R.string.CallContextMenu__info)
|
||||
) {
|
||||
val intent = when {
|
||||
peer.isCallLink -> CallLinkDetailsActivity.createIntent(fragment.requireContext(), peer.requireCallLinkRoomId())
|
||||
else -> ConversationSettingsActivity.forCall(fragment.requireContext(), peer, messageIds)
|
||||
when {
|
||||
peer.isCallLink -> callbacks.goToCallLinkDetails(peer.requireCallLinkRoomId())
|
||||
else -> fragment.startActivity(ConversationSettingsActivity.forCall(fragment.requireContext(), peer, messageIds))
|
||||
}
|
||||
fragment.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +153,7 @@ class CallLogContextMenu(
|
||||
|
||||
interface Callbacks {
|
||||
fun startSelection(call: CallLogRow)
|
||||
fun goToCallLinkDetails(roomId: CallLinkRoomId)
|
||||
fun deleteCall(call: CallLogRow)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import org.signal.core.util.orNull
|
||||
import org.thoughtcrime.securesms.MainNavigator
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.calls.links.create.CreateCallLinkBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
|
||||
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
|
||||
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
@@ -58,6 +57,7 @@ import org.thoughtcrime.securesms.main.MainToolbarMode
|
||||
import org.thoughtcrime.securesms.main.MainToolbarViewModel
|
||||
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.doAfterNextLayout
|
||||
@@ -242,9 +242,11 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
MainToolbarViewModel.Event.Search.Close -> {
|
||||
viewModel.setSearchQuery("")
|
||||
}
|
||||
|
||||
MainToolbarViewModel.Event.Search.Open -> {
|
||||
mainToolbarViewModel.setSearchHint(R.string.SearchToolbar_search)
|
||||
}
|
||||
|
||||
is MainToolbarViewModel.Event.Search.Query -> {
|
||||
viewModel.setSearchQuery(it.query.trim())
|
||||
}
|
||||
@@ -325,7 +327,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
startActivity(CallLinkDetailsActivity.createIntent(requireContext(), callLogRow.peer.requireCallLinkRoomId()))
|
||||
goToCallLinkDetails(callLogRow.peer.requireCallLinkRoomId())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,6 +377,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CallLogRow.CanStartCall.GROUP_TERMINATED -> ConversationDialogs.displayCannotStartGroupCallDueToGroupEndedDialog(requireContext())
|
||||
CallLogRow.CanStartCall.NOT_A_MEMBER -> ConversationDialogs.displayCannotStartGroupCallDueToNoLongerAMemberDialog(requireContext())
|
||||
CallLogRow.CanStartCall.ADMIN_ONLY -> ConversationDialogs.displayCannotStartGroupCallDueToPermissionsDialog(requireContext())
|
||||
@@ -386,6 +389,10 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
|
||||
viewModel.toggleSelected(call.id)
|
||||
}
|
||||
|
||||
override fun goToCallLinkDetails(roomId: CallLinkRoomId) {
|
||||
mainNavigationViewModel.goTo(MainNavigationDetailLocation.CallLinkDetails(roomId))
|
||||
}
|
||||
|
||||
override fun deleteCall(call: CallLogRow) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, 1, 1))
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
|
||||
import androidx.lifecycle.viewmodel.compose.saveable
|
||||
import org.thoughtcrime.securesms.main.MainDetailBackStack
|
||||
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
@@ -18,7 +19,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
* Controls the navigation stack used by the chats screen.
|
||||
*/
|
||||
@OptIn(SavedStateHandleSaveableApi::class)
|
||||
class ChatsBackStack(savedStateHandle: SavedStateHandle) {
|
||||
class ChatsBackStack(savedStateHandle: SavedStateHandle) : MainDetailBackStack {
|
||||
|
||||
companion object {
|
||||
private const val KEY = "chats_back_stack"
|
||||
@@ -29,7 +30,7 @@ class ChatsBackStack(savedStateHandle: SavedStateHandle) {
|
||||
)
|
||||
}
|
||||
|
||||
val entries: SnapshotStateList<MainNavigationDetailLocation> = savedStateHandle.saveable(
|
||||
override val entries: SnapshotStateList<MainNavigationDetailLocation> = savedStateHandle.saveable(
|
||||
key = KEY,
|
||||
saver = saver
|
||||
) {
|
||||
@@ -45,13 +46,10 @@ class ChatsBackStack(savedStateHandle: SavedStateHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = entries.singleOrNull() is MainNavigationDetailLocation.Empty
|
||||
|
||||
/**
|
||||
* Pushes an entry onto the stack.
|
||||
*/
|
||||
fun push(location: MainNavigationDetailLocation) {
|
||||
override fun push(location: MainNavigationDetailLocation) {
|
||||
when (location) {
|
||||
is MainNavigationDetailLocation.Empty, entries.lastOrNull() -> Unit
|
||||
|
||||
@@ -63,23 +61,4 @@ class ChatsBackStack(savedStateHandle: SavedStateHandle) {
|
||||
else -> entries.add(location)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pops the top entry off the stack. Returns true if something was popped, false if the stack is already at its root.
|
||||
*/
|
||||
fun pop(): Boolean {
|
||||
if (entries.size <= 1) return false
|
||||
entries.removeAt(entries.lastIndex)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the stack to its base empty state.
|
||||
*/
|
||||
fun reset() {
|
||||
entries.removeAll { it !is MainNavigationDetailLocation.Empty }
|
||||
if (entries.isEmpty()) {
|
||||
entries.add(MainNavigationDetailLocation.Empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.main
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.toRoute
|
||||
import org.signal.core.ui.rememberIsSplitPane
|
||||
import org.thoughtcrime.securesms.MainNavigator
|
||||
import org.thoughtcrime.securesms.calls.links.EditCallLinkNameScreen
|
||||
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsScreen
|
||||
import org.thoughtcrime.securesms.serialization.JsonSerializableNavType
|
||||
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
private val callLinkRoomIdType = typeOf<CallLinkRoomId>()
|
||||
|
||||
fun NavGraphBuilder.callNavGraphBuilder(navHostController: NavHostController) {
|
||||
composable<MainNavigationDetailLocation.Empty> {
|
||||
if (LocalResources.current.rememberIsSplitPane()) {
|
||||
EmptyDetailScreen()
|
||||
}
|
||||
}
|
||||
|
||||
composable<MainNavigationDetailLocation.CallLinkDetails>(
|
||||
typeMap = mapOf(
|
||||
callLinkRoomIdType to JsonSerializableNavType(CallLinkRoomId.serializer())
|
||||
)
|
||||
) {
|
||||
informNavigatorWeAreReady()
|
||||
|
||||
val route = it.toRoute<MainNavigationDetailLocation.CallLinkDetails>()
|
||||
|
||||
CallLinkDetailsScreen(roomId = route.callLinkRoomId)
|
||||
}
|
||||
|
||||
composable<MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName>(
|
||||
typeMap = mapOf(
|
||||
callLinkRoomIdType to JsonSerializableNavType(CallLinkRoomId.serializer())
|
||||
)
|
||||
) {
|
||||
informNavigatorWeAreReady()
|
||||
|
||||
val route = it.toRoute<MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName>()
|
||||
val parent = navHostController.previousBackStackEntry ?: return@composable
|
||||
|
||||
CompositionLocalProvider(LocalViewModelStoreOwner provides parent) {
|
||||
EditCallLinkNameScreen(roomId = route.callLinkRoomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun informNavigatorWeAreReady() {
|
||||
val navigator = LocalActivity.current as? MainNavigator.NavigatorProvider
|
||||
LaunchedEffect(navigator) {
|
||||
navigator?.onFirstRender()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.main
|
||||
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
|
||||
interface MainDetailBackStack {
|
||||
val entries: SnapshotStateList<MainNavigationDetailLocation>
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = entries.singleOrNull() is MainNavigationDetailLocation.Empty
|
||||
|
||||
fun push(location: MainNavigationDetailLocation)
|
||||
|
||||
/**
|
||||
* Pops the top entry off the stack. Returns true if something was popped, false if the stack is already at its root.
|
||||
*/
|
||||
fun pop(): Boolean {
|
||||
if (entries.size <= 1) return false
|
||||
entries.removeAt(entries.lastIndex)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the stack to its base empty state.
|
||||
*/
|
||||
fun reset() {
|
||||
entries.removeAll { it !is MainNavigationDetailLocation.Empty }
|
||||
if (entries.isEmpty()) {
|
||||
entries.add(MainNavigationDetailLocation.Empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,10 @@ sealed interface MainNavigationDetailLocation : Parcelable, NavKey {
|
||||
@Parcelize
|
||||
sealed class CallLinks : Calls {
|
||||
@Serializable
|
||||
data class EditCallLinkName(val callLinkRoomId: CallLinkRoomId) : CallLinks() {
|
||||
data class EditCallLinkName(
|
||||
val callLinkRoomId: CallLinkRoomId,
|
||||
val currentName: String = ""
|
||||
) : CallLinks() {
|
||||
@Transient
|
||||
@IgnoredOnParcel
|
||||
override val controllerKey: CallLogRow.Id = CallLogRow.Id.CallLink(callLinkRoomId)
|
||||
|
||||
@@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.reactive.asFlow
|
||||
@@ -35,6 +34,7 @@ import kotlinx.coroutines.rx3.asObservable
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.calls.CallsBackStack
|
||||
import org.thoughtcrime.securesms.calls.log.CallLogRow
|
||||
import org.thoughtcrime.securesms.chats.ChatsBackStack
|
||||
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.NotificationProfilesRepository
|
||||
@@ -86,6 +86,20 @@ class MainNavigationViewModel(
|
||||
val chatsBackStackEntries: SnapshotStateList<MainNavigationDetailLocation>
|
||||
get() = chatsBackStack.entries
|
||||
|
||||
private val callsBackStack: CallsBackStack = CallsBackStack(savedStateHandle)
|
||||
val callsBackStackEntries: SnapshotStateList<MainNavigationDetailLocation>
|
||||
get() = callsBackStack.entries
|
||||
|
||||
private val currentTabBackStack: MainDetailBackStack?
|
||||
get() {
|
||||
val currentListLocation = internalMainNavigationState.value.currentListLocation
|
||||
return when {
|
||||
currentListLocation.isChatsTab -> chatsBackStack
|
||||
currentListLocation == MainNavigationListLocation.CALLS -> callsBackStack
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private val internalDetailLocation = MutableSharedFlow<MainNavigationDetailLocation>()
|
||||
val detailLocation: SharedFlow<MainNavigationDetailLocation> = internalDetailLocation
|
||||
|
||||
@@ -97,10 +111,10 @@ class MainNavigationViewModel(
|
||||
.combine(isFullScreenPane) { id, expanded -> if (expanded) Optional.ofNullable(null) else Optional.ofNullable(id) }
|
||||
.asObservable()
|
||||
|
||||
private val internalActiveCallId = MutableStateFlow<CallLogRow.Id?>(null)
|
||||
val observableActiveCallId: Observable<Optional<out CallLogRow.Id>> = internalActiveCallId.map { Optional.ofNullable(it) }.combine(isFullScreenPane) { id, expanded ->
|
||||
if (expanded) Optional.ofNullable(null) else id
|
||||
}.asObservable()
|
||||
val observableActiveCallId: Observable<Optional<out CallLogRow.Id>> =
|
||||
snapshotFlow { callsBackStack.activeCallId }
|
||||
.combine(isFullScreenPane) { id, expanded -> if (expanded) Optional.ofNullable(null) else Optional.ofNullable(id) }
|
||||
.asObservable()
|
||||
|
||||
private val internalMegaphone = MutableStateFlow(Megaphone.NONE)
|
||||
val megaphone: StateFlow<Megaphone> = internalMegaphone
|
||||
@@ -153,12 +167,6 @@ class MainNavigationViewModel(
|
||||
performStoreUpdate(MainNavigationRepository.getHasFailedOutgoingStories()) { hasFailedStories, state ->
|
||||
state.copy(storyFailure = hasFailedStories)
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
internalDetailLocation.collect { location ->
|
||||
updateActiveStateForLocation(location)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onPaneAnchorChanged(isFullScreenPane: Boolean) {
|
||||
@@ -173,7 +181,7 @@ class MainNavigationViewModel(
|
||||
this@MainNavigationViewModel.isSplitPane = isSplitPane
|
||||
|
||||
if (!isSplitPane) {
|
||||
if (chatsBackStack.isEmpty) {
|
||||
if (currentTabBackStack?.isEmpty == true) {
|
||||
lockPaneToSecondary = true
|
||||
setFocusedPane(ThreePaneScaffoldRole.Secondary)
|
||||
}
|
||||
@@ -204,9 +212,8 @@ class MainNavigationViewModel(
|
||||
setFocusedPane(role)
|
||||
}
|
||||
|
||||
earlyNavigationDetailLocationRequested?.let { detail ->
|
||||
earlyNavigationDetailLocationRequested?.let {
|
||||
lockPaneToSecondary = false
|
||||
updateActiveStateForLocation(detail)
|
||||
}
|
||||
|
||||
return this.navigator!!
|
||||
@@ -246,20 +253,6 @@ class MainNavigationViewModel(
|
||||
*/
|
||||
override fun goTo(location: MainNavigationDetailLocation) = setDetailLocation(location)
|
||||
|
||||
private fun updateActiveStateForLocation(location: MainNavigationDetailLocation) {
|
||||
when (location) {
|
||||
is MainNavigationDetailLocation.CallLinkDetails -> {
|
||||
internalActiveCallId.update { location.controllerKey }
|
||||
}
|
||||
|
||||
is MainNavigationDetailLocation.Calls -> {
|
||||
internalActiveCallId.update { location.controllerKey }
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun MainNavigationDetailLocation.Conversation.withPreloadedWallpaper(): MainNavigationDetailLocation.Conversation {
|
||||
val args = conversationArgs
|
||||
val liveRecipient = Recipient.live(args.recipientId)
|
||||
@@ -292,12 +285,13 @@ class MainNavigationViewModel(
|
||||
|
||||
when (location) {
|
||||
is MainNavigationDetailLocation.Empty if currentListLocation.isChatsTab -> clearDetailLocation(chatsBackStack)
|
||||
is MainNavigationDetailLocation.Empty if currentListLocation == MainNavigationListLocation.CALLS -> clearDetailLocation(callsBackStack)
|
||||
is MainNavigationDetailLocation.Chats -> pushChatsDetailLocation(location)
|
||||
is MainNavigationDetailLocation.Conversation -> goToConversation(location)
|
||||
is MainNavigationDetailLocation.Calls, is MainNavigationDetailLocation.CallLinkDetails -> pushCallsDetailLocation(location)
|
||||
|
||||
is MainNavigationDetailLocation.Empty,
|
||||
is MainNavigationDetailLocation.CallLinkDetails,
|
||||
is MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName -> {
|
||||
is MainNavigationDetailLocation.Stories -> {
|
||||
if (navigator == null) {
|
||||
earlyNavigationDetailLocationRequested = location
|
||||
return
|
||||
@@ -333,15 +327,29 @@ class MainNavigationViewModel(
|
||||
setFocusedPane(ThreePaneScaffoldRole.Primary)
|
||||
}
|
||||
|
||||
fun popChatsDetailLocation() {
|
||||
chatsBackStack.pop()
|
||||
if (chatsBackStack.isEmpty) {
|
||||
fun popChatsDetailLocation() = popDetailLocation(chatsBackStack)
|
||||
|
||||
private fun pushCallsDetailLocation(location: MainNavigationDetailLocation) {
|
||||
if (location is MainNavigationDetailLocation.Calls && callsBackStack.activeCallId != location.controllerKey) {
|
||||
callsBackStack.reset()
|
||||
}
|
||||
|
||||
callsBackStack.push(location)
|
||||
setFocusedPane(ThreePaneScaffoldRole.Primary)
|
||||
}
|
||||
|
||||
fun popCallsDetailLocation() = popDetailLocation(callsBackStack)
|
||||
|
||||
private fun popDetailLocation(backStack: MainDetailBackStack) {
|
||||
backStack.pop()
|
||||
|
||||
if (backStack.isEmpty) {
|
||||
lockPaneToSecondary = true
|
||||
popDetailPane()
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearDetailLocation(backStack: ChatsBackStack) {
|
||||
private fun clearDetailLocation(backStack: MainDetailBackStack) {
|
||||
backStack.reset()
|
||||
if (!isSplitPane) {
|
||||
lockPaneToSecondary = true
|
||||
|
||||
Reference in New Issue
Block a user