mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-25 05:27:42 +00:00
Add reusable scaffold for recipient picker screens.
This commit is contained in:
committed by
Michelle Tang
parent
cf14101a24
commit
27e6ecb2a0
@@ -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) }
|
||||
)
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user