Add reusable scaffold for recipient picker screens.

This commit is contained in:
jeffrey-signal
2025-11-06 09:59:23 -05:00
committed by Michelle Tang
parent cf14101a24
commit 27e6ecb2a0
7 changed files with 222 additions and 173 deletions

View File

@@ -14,25 +14,17 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
@@ -49,25 +41,20 @@ import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.DropdownMenus
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.BlockUnblockDialog
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.compose.ScreenTitlePane
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.conversation.NewConversationUiState.UserMessage
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
import org.thoughtcrime.securesms.recipients.PhoneNumber
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.ui.RecipientPickerScaffold
import org.thoughtcrime.securesms.recipients.ui.RecipientSelection
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.window.AppScaffold
import org.thoughtcrime.securesms.window.detailPaneMaxContentWidth
import org.thoughtcrime.securesms.window.isSplitPane
import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
/**
* Allows the user to start a new conversation by selecting a recipient.
@@ -121,7 +108,7 @@ private fun NewConversationScreen(
contract = FindByActivity.Contract(),
onResult = { recipientId ->
if (recipientId != null) {
viewModel.openConversation(recipientId)
viewModel.openConversation(selection = RecipientSelection.WithId(recipientId))
}
}
)
@@ -133,10 +120,9 @@ private fun NewConversationScreen(
override fun onCreateNewGroup() = createGroupLauncher.launch(CreateGroupActivity.createIntent(context))
override fun onFindByUsername() = findByLauncher.launch(FindByMode.USERNAME)
override fun onFindByPhoneNumber() = findByLauncher.launch(FindByMode.PHONE_NUMBER)
override suspend fun shouldAllowSelection(id: RecipientId?, phone: PhoneNumber?): Boolean = true
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = viewModel.openConversation(id, phone)
override fun onPendingRecipientSelectionsConsumed() = Unit
override fun onMessage(id: RecipientId) = viewModel.openConversation(id)
override suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean = true
override fun onRecipientSelected(selection: RecipientSelection) = viewModel.openConversation(selection)
override fun onMessage(id: RecipientId) = viewModel.openConversation(RecipientSelection.WithId(id))
override fun onVoiceCall(recipient: Recipient) = CommunicationActions.startVoiceCall(context, recipient, viewModel::showUserAlreadyInACall)
override fun onVideoCall(recipient: Recipient) = CommunicationActions.startVideoCall(context, recipient, viewModel::showUserAlreadyInACall)
@@ -196,75 +182,38 @@ private suspend fun openConversation(
onComplete()
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun NewConversationScreenUi(
uiState: NewConversationUiState,
callbacks: UiCallbacks
) {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPane = uiState.forceSplitPaneOnCompactLandscape)
val snackbarHostState = remember { SnackbarHostState() }
AppScaffold(
topBarContent = {
Scaffolds.DefaultTopAppBar(
title = if (!isSplitPane) stringResource(R.string.NewConversationActivity__new_message) else "",
titleContent = { _, title -> Text(text = title, style = MaterialTheme.typography.titleLarge) },
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
navigationContentDescription = stringResource(R.string.DefaultTopAppBar__navigate_up_content_description),
onNavigationClick = callbacks::onBackPressed,
actions = { TopAppBarActions(callbacks) }
)
},
secondaryContent = {
if (isSplitPane) {
ScreenTitlePane(
title = stringResource(R.string.NewConversationActivity__new_message),
modifier = Modifier.fillMaxSize()
)
} else {
NewConversationRecipientPicker(
uiState = uiState,
callbacks = callbacks
)
}
},
RecipientPickerScaffold(
title = stringResource(R.string.NewConversationActivity__new_message),
forceSplitPane = uiState.forceSplitPaneOnCompactLandscape,
onNavigateUpClick = callbacks::onBackPressed,
topAppBarActions = { TopAppBarActions(callbacks) },
snackbarHostState = snackbarHostState,
primaryContent = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
NewConversationRecipientPicker(
uiState = uiState,
callbacks = callbacks,
modifier = Modifier.widthIn(max = windowSizeClass.detailPaneMaxContentWidth)
)
NewConversationRecipientPicker(
uiState = uiState,
callbacks = callbacks
)
UserMessagesHost(
userMessage = uiState.userMessage,
onDismiss = callbacks::onUserMessageDismissed,
onRemoveConfirmed = callbacks::onRemoveConfirmed,
onBlockConfirmed = callbacks::onBlockConfirmed,
snackbarHostState = snackbarHostState
)
if (uiState.isLookingUpRecipient) {
Dialogs.IndeterminateProgressDialog()
}
},
snackbarHost = {
SnackbarHost(snackbarHostState)
},
navigator = rememberAppScaffoldNavigator(
isSplitPane = isSplitPane
)
}
)
UserMessagesHost(
userMessage = uiState.userMessage,
onDismiss = callbacks::onUserMessageDismissed,
onRemoveConfirmed = callbacks::onRemoveConfirmed,
onBlockConfirmed = callbacks::onBlockConfirmed,
snackbarHostState = snackbarHostState
)
if (uiState.isLookingUpRecipient) {
Dialogs.IndeterminateProgressDialog()
}
}
@Composable
@@ -323,6 +272,7 @@ private interface UiCallbacks :
fun onRemoveConfirmed(recipient: Recipient)
fun onBlockConfirmed(recipient: Recipient)
fun onUserMessageDismissed(userMessage: UserMessage)
override fun onPendingRecipientSelectionsConsumed() = Unit
fun onBackPressed()
object Empty : UiCallbacks {
@@ -330,8 +280,8 @@ private interface UiCallbacks :
override fun onCreateNewGroup() = Unit
override fun onFindByUsername() = Unit
override fun onFindByPhoneNumber() = Unit
override suspend fun shouldAllowSelection(id: RecipientId?, phone: PhoneNumber?): Boolean = true
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = Unit
override suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean = true
override fun onRecipientSelected(selection: RecipientSelection) = Unit
override fun onPendingRecipientSelectionsConsumed() = Unit
override fun onMessage(id: RecipientId) = Unit
override fun onVoiceCall(recipient: Recipient) = Unit
@@ -404,7 +354,7 @@ private fun UserMessagesHost(
)
is UserMessage.Info.RecipientNotSignalUser -> Dialogs.SimpleMessageDialog(
message = stringResource(R.string.NewConversationActivity__s_is_not_a_signal_user, userMessage.phone!!.displayText),
message = stringResource(R.string.NewConversationActivity__s_is_not_a_signal_user, userMessage.phone.displayText),
dismiss = stringResource(android.R.string.ok),
onDismiss = { onDismiss(userMessage) }
)

View File

@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.recipients.PhoneNumber
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientRepository
import org.thoughtcrime.securesms.recipients.ui.RecipientSelection
class NewConversationViewModel : ViewModel() {
companion object {
@@ -41,20 +42,22 @@ class NewConversationViewModel : ViewModel() {
internalUiState.update { it.copy(searchQuery = query) }
}
fun openConversation(recipientId: RecipientId) {
private fun openConversation(recipientId: RecipientId) {
internalUiState.update { it.copy(pendingDestination = recipientId) }
}
fun openConversation(id: RecipientId?, phone: PhoneNumber?) {
when {
id != null -> openConversation(recipientId = id)
SignalStore.account.isRegistered -> {
Log.d(TAG, "[openConversation] Missing recipientId: attempting to look up.")
resolveAndOpenConversation(phone!!)
fun openConversation(selection: RecipientSelection) {
when (selection) {
is RecipientSelection.WithId -> openConversation(recipientId = selection.id)
is RecipientSelection.WithIdAndPhone -> openConversation(recipientId = selection.id)
is RecipientSelection.WithPhone -> {
if (SignalStore.account.isRegistered) {
Log.d(TAG, "[openConversation] Missing recipientId: attempting to look up.")
resolveAndOpenConversation(selection.phone)
} else {
Log.w(TAG, "[openConversation] Cannot look up recipient: account not registered.")
}
}
else -> Log.w(TAG, "[openConversation] Cannot look up recipient: account not registered.")
}
}

View File

@@ -55,6 +55,7 @@ import org.thoughtcrime.securesms.groups.SelectionLimits
import org.thoughtcrime.securesms.recipients.PhoneNumber
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.ui.RecipientSelection
import java.util.Optional
import java.util.function.Consumer
@@ -244,13 +245,20 @@ private fun ContactSelectionListFragment.setUpCallbacks(
chatType: Optional<ChatType?>,
resultConsumer: Consumer<Boolean?>
) {
val recipientId = recipientId.orNull()
val id = recipientId.orNull()
val phone = number?.let(::PhoneNumber)
val selection = when {
id != null && phone != null -> RecipientSelection.WithIdAndPhone(id, phone)
id != null -> RecipientSelection.WithId(id)
phone != null -> RecipientSelection.WithPhone(phone)
else -> error("Either RecipientId or PhoneNumber must be non-null")
}
coroutineScope.launch {
val shouldAllowSelection = callbacks.listActions.shouldAllowSelection(recipientId, phone)
val shouldAllowSelection = callbacks.listActions.shouldAllowSelection(selection)
if (shouldAllowSelection) {
callbacks.listActions.onRecipientSelected(recipientId, phone)
callbacks.listActions.onRecipientSelected(selection)
}
resultConsumer.accept(shouldAllowSelection)
}
@@ -378,16 +386,16 @@ data class RecipientPickerCallbacks(
* This is called before [onRecipientSelected] to provide a chance to prevent the selection.
*/
fun onSearchQueryChanged(query: String)
suspend fun shouldAllowSelection(id: RecipientId?, phone: PhoneNumber?): Boolean
fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?)
suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean
fun onRecipientSelected(selection: RecipientSelection)
fun onSelectionChanged(newSelections: List<SelectedContact>, totalMembersCount: Int) = Unit
fun onPendingRecipientSelectionsConsumed()
fun onContactsListReset() = Unit
object Empty : ListActions {
override fun onSearchQueryChanged(query: String) = Unit
override suspend fun shouldAllowSelection(id: RecipientId?, phone: PhoneNumber?): Boolean = true
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = Unit
override suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean = true
override fun onRecipientSelected(selection: RecipientSelection) = Unit
override fun onPendingRecipientSelectionsConsumed() = Unit
override fun onContactsListReset() = Unit
}

View File

@@ -24,16 +24,12 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -52,11 +48,9 @@ import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.compose.ScreenTitlePane
import org.thoughtcrime.securesms.contacts.SelectedContact
import org.thoughtcrime.securesms.conversation.RecipientPicker
import org.thoughtcrime.securesms.conversation.RecipientPickerCallbacks
@@ -64,14 +58,10 @@ import org.thoughtcrime.securesms.groups.SelectionLimits
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupUiState.NavTarget
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupUiState.UserMessage
import org.thoughtcrime.securesms.groups.ui.creategroup.details.AddGroupDetailsActivity
import org.thoughtcrime.securesms.recipients.PhoneNumber
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.ui.RecipientPickerScaffold
import org.thoughtcrime.securesms.recipients.ui.RecipientSelection
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode
import org.thoughtcrime.securesms.window.AppScaffold
import org.thoughtcrime.securesms.window.detailPaneMaxContentWidth
import org.thoughtcrime.securesms.window.isSplitPane
import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
import java.text.NumberFormat
/**
@@ -128,7 +118,7 @@ private fun CreateGroupScreen(
override fun onSearchQueryChanged(query: String) = viewModel.onSearchQueryChanged(query)
override fun onFindByUsername() = findByLauncher.launch(FindByMode.USERNAME)
override fun onFindByPhoneNumber() = findByLauncher.launch(FindByMode.PHONE_NUMBER)
override suspend fun shouldAllowSelection(id: RecipientId?, phone: PhoneNumber?): Boolean = viewModel.shouldAllowSelection(id, phone)
override suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean = viewModel.shouldAllowSelection(selection)
override fun onSelectionChanged(newSelections: List<SelectedContact>, totalMembersCount: Int) = viewModel.onSelectionChanged(newSelections, totalMembersCount)
override fun onPendingRecipientSelectionsConsumed() = viewModel.clearPendingRecipientSelections()
override fun onNextClicked(): Unit = viewModel.continueToGroupDetails()
@@ -164,11 +154,7 @@ private fun CreateGroupScreenUi(
uiState: CreateGroupUiState,
callbacks: UiCallbacks
) {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPane = uiState.forceSplitPane)
val snackbarHostState = remember { SnackbarHostState() }
val titleText = if (uiState.newSelections.isNotEmpty()) {
val title = if (uiState.newSelections.isNotEmpty()) {
pluralStringResource(
id = R.plurals.CreateGroupActivity__s_members,
count = uiState.totalMembersCount,
@@ -178,61 +164,28 @@ private fun CreateGroupScreenUi(
stringResource(R.string.CreateGroupActivity__select_members)
}
AppScaffold(
topBarContent = {
Scaffolds.DefaultTopAppBar(
title = if (!isSplitPane) titleText else "",
titleContent = { _, title -> Text(text = title, style = MaterialTheme.typography.titleLarge) },
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
navigationContentDescription = stringResource(R.string.DefaultTopAppBar__navigate_up_content_description),
onNavigationClick = callbacks::onBackPressed
)
},
secondaryContent = {
if (isSplitPane) {
ScreenTitlePane(
title = titleText,
modifier = Modifier.fillMaxSize()
)
} else {
CreateGroupRecipientPicker(
uiState = uiState,
callbacks = callbacks
)
}
},
RecipientPickerScaffold(
title = title,
forceSplitPane = uiState.forceSplitPane,
onNavigateUpClick = callbacks::onBackPressed,
topAppBarActions = {},
snackbarHostState = remember { SnackbarHostState() },
primaryContent = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
CreateGroupRecipientPicker(
uiState = uiState,
callbacks = callbacks,
modifier = Modifier.widthIn(max = windowSizeClass.detailPaneMaxContentWidth)
)
CreateGroupRecipientPicker(
uiState = uiState,
callbacks = callbacks
)
UserMessagesHost(
userMessage = uiState.userMessage,
onDismiss = callbacks::onUserMessageDismissed
)
if (uiState.isLookingUpRecipient) {
Dialogs.IndeterminateProgressDialog()
}
},
snackbarHost = {
SnackbarHost(snackbarHostState)
},
navigator = rememberAppScaffoldNavigator(
isSplitPane = isSplitPane
)
}
)
UserMessagesHost(
userMessage = uiState.userMessage,
onDismiss = callbacks::onUserMessageDismissed
)
if (uiState.isLookingUpRecipient) {
Dialogs.IndeterminateProgressDialog()
}
}
@Composable
@@ -296,7 +249,7 @@ private interface UiCallbacks :
RecipientPickerCallbacks.FindByUsername,
RecipientPickerCallbacks.FindByPhoneNumber {
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = Unit
override fun onRecipientSelected(selection: RecipientSelection) = Unit
fun onNextClicked()
fun onUserMessageDismissed(userMessage: UserMessage)
fun onBackPressed()
@@ -306,7 +259,7 @@ private interface UiCallbacks :
override fun onSearchQueryChanged(query: String) = Unit
override fun onFindByUsername() = Unit
override fun onFindByPhoneNumber() = Unit
override suspend fun shouldAllowSelection(id: RecipientId?, phone: PhoneNumber?): Boolean = true
override suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean = true
override fun onPendingRecipientSelectionsConsumed() = Unit
override fun onNextClicked() = Unit
override fun onUserMessageDismissed(userMessage: UserMessage) = Unit

View File

@@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.recipients.PhoneNumber
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientRepository
import org.thoughtcrime.securesms.recipients.ui.RecipientSelection
import org.thoughtcrime.securesms.util.RemoteConfig
import java.io.IOException
import kotlin.time.Duration.Companion.seconds
@@ -43,8 +44,9 @@ class CreateGroupViewModel : ViewModel() {
internalUiState.update { it.copy(searchQuery = query) }
}
suspend fun shouldAllowSelection(id: RecipientId?, phone: PhoneNumber?): Boolean {
return if (id != null) true else recipientExists(phone!!)
suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean = when (selection) {
is RecipientSelection.WithId, is RecipientSelection.WithIdAndPhone -> true
is RecipientSelection.WithPhone -> recipientExists(selection.phone)
}
private suspend fun recipientExists(phone: PhoneNumber): Boolean {

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.recipients.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import org.signal.core.ui.compose.AllDevicePreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.compose.ScreenTitlePane
import org.thoughtcrime.securesms.window.AppScaffold
import org.thoughtcrime.securesms.window.detailPaneMaxContentWidth
import org.thoughtcrime.securesms.window.isSplitPane
import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
/**
* Provides the common adaptive layout structure for recipient picker screens.
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun RecipientPickerScaffold(
title: String,
forceSplitPane: Boolean,
onNavigateUpClick: () -> Unit,
topAppBarActions: @Composable () -> Unit,
snackbarHostState: SnackbarHostState,
primaryContent: @Composable () -> Unit
) {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPane = forceSplitPane)
AppScaffold(
topBarContent = {
Scaffolds.DefaultTopAppBar(
title = if (!isSplitPane) title else "",
titleContent = { _, titleText -> Text(text = titleText, style = MaterialTheme.typography.titleLarge) },
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
navigationContentDescription = stringResource(R.string.DefaultTopAppBar__navigate_up_content_description),
onNavigationClick = onNavigateUpClick,
actions = { topAppBarActions() }
)
},
secondaryContent = {
if (isSplitPane) {
ScreenTitlePane(
title = title,
modifier = Modifier.fillMaxSize()
)
} else {
primaryContent()
}
},
primaryContent = {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Box(modifier = Modifier.widthIn(max = windowSizeClass.detailPaneMaxContentWidth)) {
primaryContent()
}
}
},
snackbarHost = {
SnackbarHost(snackbarHostState)
},
navigator = rememberAppScaffoldNavigator(
isSplitPane = isSplitPane
)
)
}
@AllDevicePreviews
@Composable
private fun RecipientPickerScaffoldPreview() {
Previews.Preview {
RecipientPickerScaffold(
title = "Screen Title",
forceSplitPane = false,
onNavigateUpClick = {},
topAppBarActions = {},
snackbarHostState = SnackbarHostState(),
primaryContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Gray),
contentAlignment = Alignment.Center
) {
Text(text = "primaryContent")
}
}
)
}
}

View File

@@ -0,0 +1,15 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.recipients.ui
import org.thoughtcrime.securesms.recipients.PhoneNumber
import org.thoughtcrime.securesms.recipients.RecipientId
sealed interface RecipientSelection {
data class WithId(val id: RecipientId) : RecipientSelection
data class WithPhone(val phone: PhoneNumber) : RecipientSelection
data class WithIdAndPhone(val id: RecipientId, val phone: PhoneNumber) : RecipientSelection
}