mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01: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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user