mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-21 10:17:56 +00:00
New conversation v2 - Add support for find by username/phone/contacts and group creation.
This commit is contained in:
committed by
Cody Henthorne
parent
33f9369883
commit
5d60ab35de
@@ -146,7 +146,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
super.onAttach(context);
|
||||
|
||||
if (context instanceof NewConversationCallback) {
|
||||
newConversationCallback = (NewConversationCallback) context;
|
||||
setNewConversationCallback((NewConversationCallback) context);
|
||||
}
|
||||
|
||||
if (context instanceof FindByCallback) {
|
||||
@@ -198,6 +198,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
}
|
||||
}
|
||||
|
||||
public void setNewConversationCallback(@Nullable NewConversationCallback callback) {
|
||||
this.newConversationCallback = callback;
|
||||
}
|
||||
|
||||
public void setFindByCallback(@Nullable FindByCallback callback) {
|
||||
this.findByCallback = callback;
|
||||
}
|
||||
@@ -903,7 +907,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_CONTACTS_BANNER.getCode());
|
||||
}
|
||||
|
||||
if (newConversationCallback != null && !hasQuery) {
|
||||
if (fragmentArgs.getEnableCreateNewGroup() && !hasQuery) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
data class ContactSelectionArguments(
|
||||
val displayMode: Int = Defaults.DISPLAY_MODE,
|
||||
val isRefreshable: Boolean = Defaults.IS_REFRESHABLE,
|
||||
val enableCreateNewGroup: Boolean = Defaults.ENABLE_CREATE_NEW_GROUP,
|
||||
val enableFindByUsername: Boolean = Defaults.ENABLE_FIND_BY_USERNAME,
|
||||
val enableFindByPhoneNumber: Boolean = Defaults.ENABLE_FIND_BY_PHONE_NUMBER,
|
||||
val includeRecents: Boolean = Defaults.INCLUDE_RECENTS,
|
||||
@@ -30,6 +31,7 @@ data class ContactSelectionArguments(
|
||||
return Bundle().apply {
|
||||
putInt(DISPLAY_MODE, displayMode)
|
||||
putBoolean(REFRESHABLE, isRefreshable)
|
||||
putBoolean(ENABLE_CREATE_NEW_GROUP, enableCreateNewGroup)
|
||||
putBoolean(ENABLE_FIND_BY_USERNAME, enableFindByUsername)
|
||||
putBoolean(ENABLE_FIND_BY_PHONE_NUMBER, enableFindByPhoneNumber)
|
||||
putBoolean(RECENTS, includeRecents)
|
||||
@@ -46,6 +48,7 @@ data class ContactSelectionArguments(
|
||||
companion object {
|
||||
const val DISPLAY_MODE = "display_mode"
|
||||
const val REFRESHABLE = "refreshable"
|
||||
const val ENABLE_CREATE_NEW_GROUP = "enable_create_new_group"
|
||||
const val ENABLE_FIND_BY_USERNAME = "enable_find_by_username"
|
||||
const val ENABLE_FIND_BY_PHONE_NUMBER = "enable_find_by_phone"
|
||||
const val RECENTS = "recents"
|
||||
@@ -69,6 +72,7 @@ data class ContactSelectionArguments(
|
||||
return ContactSelectionArguments(
|
||||
displayMode = bundle.getInt(DISPLAY_MODE, intent.getIntExtra(DISPLAY_MODE, Defaults.DISPLAY_MODE)),
|
||||
isRefreshable = bundle.getBoolean(REFRESHABLE, intent.getBooleanExtra(REFRESHABLE, Defaults.IS_REFRESHABLE)),
|
||||
enableCreateNewGroup = bundle.getBoolean(ENABLE_CREATE_NEW_GROUP, intent.getBooleanExtra(ENABLE_CREATE_NEW_GROUP, Defaults.ENABLE_CREATE_NEW_GROUP)),
|
||||
enableFindByUsername = bundle.getBoolean(ENABLE_FIND_BY_USERNAME, intent.getBooleanExtra(ENABLE_FIND_BY_USERNAME, Defaults.ENABLE_FIND_BY_USERNAME)),
|
||||
enableFindByPhoneNumber = bundle.getBoolean(ENABLE_FIND_BY_PHONE_NUMBER, intent.getBooleanExtra(ENABLE_FIND_BY_PHONE_NUMBER, Defaults.ENABLE_FIND_BY_PHONE_NUMBER)),
|
||||
includeRecents = bundle.getBoolean(RECENTS, intent.getBooleanExtra(RECENTS, Defaults.INCLUDE_RECENTS)),
|
||||
@@ -87,6 +91,7 @@ data class ContactSelectionArguments(
|
||||
private object Defaults {
|
||||
const val DISPLAY_MODE = ContactSelectionDisplayMode.FLAG_ALL
|
||||
const val IS_REFRESHABLE = true
|
||||
const val ENABLE_CREATE_NEW_GROUP = false
|
||||
const val ENABLE_FIND_BY_USERNAME = false
|
||||
const val ENABLE_FIND_BY_PHONE_NUMBER = false
|
||||
const val INCLUDE_RECENTS = false
|
||||
|
||||
@@ -5,10 +5,14 @@
|
||||
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
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
|
||||
@@ -18,22 +22,34 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import org.signal.core.ui.compose.AllDevicePreviews
|
||||
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.util.viewModel
|
||||
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.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity
|
||||
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode
|
||||
import org.thoughtcrime.securesms.window.AppScaffoldWithTopBar
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||
import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
|
||||
@@ -49,29 +65,103 @@ class NewConversationActivityV2 : PassphraseRequiredActivity() {
|
||||
fun createIntent(context: Context): Intent = Intent(context, NewConversationActivityV2::class.java)
|
||||
}
|
||||
|
||||
private val viewModel by viewModel { NewConversationViewModel() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
super.onCreate(savedInstanceState, ready)
|
||||
|
||||
setContent {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val navigateBack = onBackPressedDispatcher::onBackPressed
|
||||
|
||||
setContent {
|
||||
SignalTheme {
|
||||
NewConversationScreen(
|
||||
uiState = uiState,
|
||||
callbacks = object : Callbacks {
|
||||
override fun onBackPressed() = onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
activityIntent = intent,
|
||||
closeScreen = navigateBack
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
private fun NewConversationScreen(
|
||||
viewModel: NewConversationViewModel = viewModel { NewConversationViewModel() },
|
||||
activityIntent: Intent,
|
||||
closeScreen: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val createGroupLauncher: ActivityResultLauncher<Intent> = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
closeScreen()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val findByLauncher: ActivityResultLauncher<FindByMode> = rememberLauncherForActivityResult(
|
||||
contract = FindByActivity.Contract(),
|
||||
onResult = { recipientId ->
|
||||
if (recipientId != null) {
|
||||
viewModel.onMessage(recipientId)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val callbacks = remember {
|
||||
object : Callbacks {
|
||||
override fun onCreateNewGroup() = createGroupLauncher.launch(CreateGroupActivity.newIntent(context))
|
||||
override fun onFindByUsername() = findByLauncher.launch(FindByMode.USERNAME)
|
||||
override fun onFindByPhoneNumber() = findByLauncher.launch(FindByMode.PHONE_NUMBER)
|
||||
override fun shouldAllowSelection(id: RecipientId): Boolean = true
|
||||
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = viewModel.onRecipientSelected(id, phone)
|
||||
override fun onMessage(id: RecipientId) = viewModel.onMessage(id)
|
||||
override fun onInviteToSignal() = context.startActivity(AppSettingsActivity.invite(context))
|
||||
override fun onUserMessageDismissed(userMessage: UserMessage) = viewModel.onUserMessageDismissed()
|
||||
override fun onBackPressed() = closeScreen()
|
||||
}
|
||||
}
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(uiState.pendingDestination) {
|
||||
uiState.pendingDestination?.let { recipientId ->
|
||||
openConversation(context, recipientId, activityIntent, onComplete = closeScreen)
|
||||
}
|
||||
}
|
||||
|
||||
NewConversationScreenUi(
|
||||
uiState = uiState,
|
||||
callbacks = callbacks
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun openConversation(
|
||||
context: Context,
|
||||
recipientId: RecipientId,
|
||||
activityIntent: Intent? = null,
|
||||
onComplete: () -> Unit = {}
|
||||
) {
|
||||
val intent: Intent = ConversationIntents.createBuilder(context, recipientId, -1L)
|
||||
.map { builder ->
|
||||
if (activityIntent != null) {
|
||||
builder
|
||||
.withDraftText(activityIntent.getStringExtra(Intent.EXTRA_TEXT))
|
||||
.withDataUri(activityIntent.data)
|
||||
.withDataType(activityIntent.type)
|
||||
.build()
|
||||
} else {
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
.await()
|
||||
|
||||
context.startActivity(intent)
|
||||
onComplete()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
private fun NewConversationScreenUi(
|
||||
uiState: NewConversationUiState,
|
||||
callbacks: Callbacks
|
||||
) {
|
||||
@@ -88,6 +178,7 @@ private fun NewConversationScreen(
|
||||
onNavigationClick = callbacks::onBackPressed
|
||||
)
|
||||
},
|
||||
|
||||
secondaryContent = {
|
||||
if (isSplitPane) {
|
||||
ScreenTitlePane(
|
||||
@@ -95,7 +186,9 @@ private fun NewConversationScreen(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
} else {
|
||||
RecipientPicker()
|
||||
NewConversationRecipientPicker(
|
||||
callbacks = callbacks
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -104,9 +197,9 @@ private fun NewConversationScreen(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
RecipientPicker(
|
||||
modifier = Modifier
|
||||
.widthIn(max = windowSizeClass.detailPaneMaxContentWidth)
|
||||
NewConversationRecipientPicker(
|
||||
callbacks = callbacks,
|
||||
modifier = Modifier.widthIn(max = windowSizeClass.detailPaneMaxContentWidth)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -115,35 +208,77 @@ private fun NewConversationScreen(
|
||||
isSplitPane = isSplitPane
|
||||
)
|
||||
)
|
||||
|
||||
UserMessagesHost(
|
||||
userMessage = uiState.userMessage,
|
||||
onDismiss = callbacks::onUserMessageDismissed
|
||||
)
|
||||
|
||||
if (uiState.isRefreshingRecipient) {
|
||||
Dialogs.IndeterminateProgressDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private interface Callbacks {
|
||||
private interface Callbacks : RecipientPickerCallbacks {
|
||||
fun onUserMessageDismissed(userMessage: UserMessage)
|
||||
fun onBackPressed()
|
||||
|
||||
object Empty : Callbacks {
|
||||
override fun onCreateNewGroup() = Unit
|
||||
override fun onFindByUsername() = Unit
|
||||
override fun onFindByPhoneNumber() = Unit
|
||||
override fun shouldAllowSelection(id: RecipientId): Boolean = true
|
||||
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = Unit
|
||||
override fun onMessage(id: RecipientId) = Unit
|
||||
override fun onInviteToSignal() = Unit
|
||||
override fun onUserMessageDismissed(userMessage: UserMessage) = Unit
|
||||
override fun onBackPressed() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RecipientPicker(
|
||||
private fun NewConversationRecipientPicker(
|
||||
callbacks: Callbacks,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
RecipientPicker(
|
||||
enableCreateNewGroup = true,
|
||||
enableFindByUsername = true,
|
||||
enableFindByPhoneNumber = true,
|
||||
callbacks = RecipientPickerCallbacks.Empty, // TODO(jeffrey) implement callbacks
|
||||
callbacks = callbacks,
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserMessagesHost(
|
||||
userMessage: UserMessage?,
|
||||
onDismiss: (UserMessage) -> Unit
|
||||
) {
|
||||
when (userMessage) {
|
||||
null -> {}
|
||||
|
||||
UserMessage.NetworkError -> Dialogs.SimpleMessageDialog(
|
||||
message = stringResource(R.string.NetworkFailure__network_error_check_your_connection_and_try_again),
|
||||
dismiss = stringResource(android.R.string.ok),
|
||||
onDismiss = { onDismiss(userMessage) }
|
||||
)
|
||||
|
||||
is UserMessage.RecipientNotSignalUser -> Dialogs.SimpleMessageDialog(
|
||||
message = stringResource(R.string.NewConversationActivity__s_is_not_a_signal_user, userMessage.phone!!.displayText),
|
||||
dismiss = stringResource(android.R.string.ok),
|
||||
onDismiss = { onDismiss(userMessage) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@AllDevicePreviews
|
||||
@Composable
|
||||
private fun NewConversationScreenPreview() {
|
||||
Previews.Preview {
|
||||
NewConversationScreen(
|
||||
NewConversationScreenUi(
|
||||
uiState = NewConversationUiState(
|
||||
forceSplitPaneOnCompactLandscape = false
|
||||
),
|
||||
|
||||
@@ -6,16 +6,107 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.conversation.NewConversationUiState.UserMessage
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.PhoneNumber
|
||||
import org.thoughtcrime.securesms.recipients.Recipient.Companion.resolved
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.RecipientRepository
|
||||
|
||||
class NewConversationViewModel : ViewModel() {
|
||||
companion object {
|
||||
private val TAG = Log.tag(NewConversationViewModel::class)
|
||||
}
|
||||
|
||||
private val _uiState = MutableStateFlow(NewConversationUiState())
|
||||
val uiState: StateFlow<NewConversationUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun onMessage(id: RecipientId): Unit = openConversation(recipientId = id)
|
||||
|
||||
fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) {
|
||||
when {
|
||||
id != null -> openConversation(recipientId = id)
|
||||
|
||||
SignalStore.account.isRegistered -> {
|
||||
Log.d(TAG, "[onRecipientSelected] Missing recipientId: attempting to look up.")
|
||||
resolveAndOpenConversation(phone)
|
||||
}
|
||||
|
||||
else -> Log.w(TAG, "[onRecipientSelected] Cannot look up recipient: account not registered.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun openConversation(recipientId: RecipientId) {
|
||||
_uiState.update { it.copy(pendingDestination = recipientId) }
|
||||
}
|
||||
|
||||
private fun resolveAndOpenConversation(phone: PhoneNumber?) {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isRefreshingRecipient = true) }
|
||||
|
||||
val lookupResult = withContext(Dispatchers.IO) {
|
||||
if (phone != null) {
|
||||
RecipientRepository.lookupNewE164(inputE164 = phone.value)
|
||||
} else {
|
||||
RecipientRepository.LookupResult.InvalidEntry
|
||||
}
|
||||
}
|
||||
|
||||
when (lookupResult) {
|
||||
is RecipientRepository.LookupResult.Success -> {
|
||||
val recipient = resolved(lookupResult.recipientId)
|
||||
_uiState.update { it.copy(isRefreshingRecipient = false) }
|
||||
|
||||
if (recipient.isRegistered && recipient.hasServiceId) {
|
||||
openConversation(recipient.id)
|
||||
} else {
|
||||
Log.d(TAG, "[resolveAndOpenConversation] Lookup successful, but recipient is not registered or has no service ID.")
|
||||
}
|
||||
}
|
||||
|
||||
is RecipientRepository.LookupResult.NotFound, is RecipientRepository.LookupResult.InvalidEntry -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isRefreshingRecipient = false,
|
||||
userMessage = UserMessage.RecipientNotSignalUser(phone)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is RecipientRepository.LookupResult.NetworkError -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isRefreshingRecipient = false,
|
||||
userMessage = UserMessage.NetworkError
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onUserMessageDismissed() {
|
||||
_uiState.update { it.copy(userMessage = null) }
|
||||
}
|
||||
}
|
||||
|
||||
data class NewConversationUiState(
|
||||
val forceSplitPaneOnCompactLandscape: Boolean = SignalStore.internal.forceSplitPaneOnCompactLandscape
|
||||
)
|
||||
val forceSplitPaneOnCompactLandscape: Boolean = SignalStore.internal.forceSplitPaneOnCompactLandscape,
|
||||
val isRefreshingRecipient: Boolean = false,
|
||||
val pendingDestination: RecipientId? = null,
|
||||
val userMessage: UserMessage? = null
|
||||
) {
|
||||
sealed interface UserMessage {
|
||||
data class RecipientNotSignalUser(val phone: PhoneNumber?) : UserMessage
|
||||
data object NetworkError : UserMessage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +27,19 @@ import org.signal.core.ui.compose.DayNightPreviews
|
||||
import org.signal.core.ui.compose.Fragments
|
||||
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||
import org.thoughtcrime.securesms.components.ContactFilterView
|
||||
import org.thoughtcrime.securesms.contacts.paged.ChatType
|
||||
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments
|
||||
import org.thoughtcrime.securesms.recipients.PhoneNumber
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import java.util.Optional
|
||||
import java.util.function.Consumer
|
||||
|
||||
/**
|
||||
* Provides a recipient search and selection UI.
|
||||
*/
|
||||
@Composable
|
||||
fun RecipientPicker(
|
||||
enableCreateNewGroup: Boolean,
|
||||
enableFindByUsername: Boolean,
|
||||
enableFindByPhoneNumber: Boolean,
|
||||
callbacks: RecipientPickerCallbacks,
|
||||
@@ -56,6 +61,7 @@ fun RecipientPicker(
|
||||
|
||||
RecipientSearchResultsList(
|
||||
searchQuery = searchQuery,
|
||||
enableCreateNewGroup = enableCreateNewGroup,
|
||||
enableFindByUsername = enableFindByUsername,
|
||||
enableFindByPhoneNumber = enableFindByPhoneNumber,
|
||||
callbacks = callbacks,
|
||||
@@ -100,12 +106,14 @@ private fun RecipientSearchField(
|
||||
@Composable
|
||||
private fun RecipientSearchResultsList(
|
||||
searchQuery: String,
|
||||
enableCreateNewGroup: Boolean,
|
||||
enableFindByUsername: Boolean,
|
||||
enableFindByPhoneNumber: Boolean,
|
||||
callbacks: RecipientPickerCallbacks,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val fragmentArgs = ContactSelectionArguments(
|
||||
enableCreateNewGroup = enableCreateNewGroup,
|
||||
enableFindByUsername = enableFindByUsername,
|
||||
enableFindByPhoneNumber = enableFindByPhoneNumber
|
||||
).toArgumentBundle()
|
||||
@@ -118,12 +126,13 @@ private fun RecipientSearchResultsList(
|
||||
fragmentState = fragmentState,
|
||||
onUpdate = { fragment ->
|
||||
currentFragment = fragment
|
||||
currentFragment?.view?.setPadding(0, 0, 0, 0)
|
||||
|
||||
fragment.setFindByCallback(object : ContactSelectionListFragment.FindByCallback {
|
||||
override fun onFindByUsername() = callbacks.onFindByUsername()
|
||||
override fun onFindByPhoneNumber() = callbacks.onFindByPhoneNumber()
|
||||
})
|
||||
fragment.view?.setPadding(0, 0, 0, 0)
|
||||
fragment.setUpCallbacks(
|
||||
callbacks = callbacks,
|
||||
enableCreateNewGroup = enableCreateNewGroup,
|
||||
enableFindByUsername = enableFindByUsername,
|
||||
enableFindByPhoneNumber = enableFindByPhoneNumber
|
||||
)
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
@@ -141,10 +150,57 @@ private fun RecipientSearchResultsList(
|
||||
}
|
||||
}
|
||||
|
||||
private fun ContactSelectionListFragment.setUpCallbacks(
|
||||
callbacks: RecipientPickerCallbacks,
|
||||
enableCreateNewGroup: Boolean,
|
||||
enableFindByUsername: Boolean,
|
||||
enableFindByPhoneNumber: Boolean
|
||||
) {
|
||||
val fragment: ContactSelectionListFragment = this
|
||||
|
||||
if (enableCreateNewGroup) {
|
||||
fragment.setNewConversationCallback(object : ContactSelectionListFragment.NewConversationCallback {
|
||||
override fun onInvite() = callbacks.onInviteToSignal()
|
||||
override fun onNewGroup(forceV1: Boolean) = callbacks.onCreateNewGroup()
|
||||
})
|
||||
}
|
||||
|
||||
if (enableFindByUsername || enableFindByPhoneNumber) {
|
||||
fragment.setFindByCallback(object : ContactSelectionListFragment.FindByCallback {
|
||||
override fun onFindByUsername() = callbacks.onFindByUsername()
|
||||
override fun onFindByPhoneNumber() = callbacks.onFindByPhoneNumber()
|
||||
})
|
||||
}
|
||||
|
||||
fragment.setOnContactSelectedListener(object : ContactSelectionListFragment.OnContactSelectedListener {
|
||||
override fun onBeforeContactSelected(
|
||||
isFromUnknownSearchKey: Boolean,
|
||||
recipientId: Optional<RecipientId?>,
|
||||
number: String?,
|
||||
chatType: Optional<ChatType?>,
|
||||
resultConsumer: Consumer<Boolean?>
|
||||
) {
|
||||
val recipientId = recipientId.get()
|
||||
val shouldAllowSelection = callbacks.shouldAllowSelection(recipientId)
|
||||
if (shouldAllowSelection) {
|
||||
callbacks.onRecipientSelected(
|
||||
id = recipientId,
|
||||
phone = number?.let(::PhoneNumber)
|
||||
)
|
||||
}
|
||||
resultConsumer.accept(shouldAllowSelection)
|
||||
}
|
||||
|
||||
override fun onContactDeselected(recipientId: Optional<RecipientId?>, number: String?, chatType: Optional<ChatType?>) = Unit
|
||||
override fun onSelectionChanged() = Unit
|
||||
})
|
||||
}
|
||||
|
||||
@DayNightPreviews
|
||||
@Composable
|
||||
private fun RecipientPickerPreview() {
|
||||
RecipientPicker(
|
||||
enableCreateNewGroup = true,
|
||||
enableFindByUsername = true,
|
||||
enableFindByPhoneNumber = true,
|
||||
callbacks = RecipientPickerCallbacks.Empty
|
||||
@@ -152,13 +208,27 @@ private fun RecipientPickerPreview() {
|
||||
}
|
||||
|
||||
interface RecipientPickerCallbacks {
|
||||
fun onCreateNewGroup()
|
||||
fun onFindByUsername()
|
||||
fun onFindByPhoneNumber()
|
||||
fun onRecipientClick(id: RecipientId)
|
||||
|
||||
/**
|
||||
* Validates whether the selection of [RecipientId] should be allowed. Return true if the selection can proceed, false otherwise.
|
||||
*
|
||||
* This is called before [onRecipientSelected] to provide a chance to prevent the selection.
|
||||
*/
|
||||
fun shouldAllowSelection(id: RecipientId): Boolean
|
||||
fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?)
|
||||
fun onMessage(id: RecipientId)
|
||||
fun onInviteToSignal()
|
||||
|
||||
object Empty : RecipientPickerCallbacks {
|
||||
override fun onCreateNewGroup() = Unit
|
||||
override fun onFindByUsername() = Unit
|
||||
override fun onFindByPhoneNumber() = Unit
|
||||
override fun onRecipientClick(id: RecipientId) = Unit
|
||||
override fun shouldAllowSelection(id: RecipientId): Boolean = true
|
||||
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = Unit
|
||||
override fun onMessage(id: RecipientId) = Unit
|
||||
override fun onInviteToSignal() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.recipients
|
||||
|
||||
import org.thoughtcrime.securesms.util.SignalE164Util
|
||||
|
||||
@JvmInline
|
||||
value class PhoneNumber(val value: String) {
|
||||
val displayText: String
|
||||
get() = SignalE164Util.prettyPrint(value)
|
||||
}
|
||||
Reference in New Issue
Block a user