diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 88bff81e9c..47712408f9 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -713,11 +713,10 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
-
+
, number: String?, chatType: Optional, callback: Consumer) {
- if (recipientId.isPresent) {
- launch(Recipient.resolved(recipientId.get()))
- } else {
- Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.")
- if (SignalStore.account.isRegistered) {
- Log.i(TAG, "[onContactSelected] Doing contact refresh.")
-
- val progress = SimpleProgressDialog.show(this)
-
- SimpleTask.run(lifecycle, { RecipientRepository.lookupNewE164(number!!) }, { result ->
- progress.dismiss()
-
- when (result) {
- is RecipientRepository.LookupResult.Success -> {
- val resolved = Recipient.resolved(result.recipientId)
- if (resolved.isRegistered && resolved.hasServiceId) {
- launch(resolved)
- }
- }
-
- is RecipientRepository.LookupResult.NotFound,
- is RecipientRepository.LookupResult.InvalidEntry -> {
- MaterialAlertDialogBuilder(this)
- .setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, number))
- .setPositiveButton(android.R.string.ok, null)
- .show()
- }
-
- else -> {
- MaterialAlertDialogBuilder(this)
- .setMessage(R.string.NetworkFailure__network_error_check_your_connection_and_try_again)
- .setPositiveButton(android.R.string.ok, null)
- .show()
- }
- }
- })
- }
- }
- callback.accept(true)
- }
-
- private fun launch(recipient: Recipient) {
- if (recipient.isGroup) {
- CommunicationActions.startVideoCall(this, recipient) {
- YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content))
- }
- } else {
- CommunicationActions.startVoiceCall(this, recipient) {
- YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content))
- }
- }
- }
+/**
+ * Allows the user to start a new call by selecting a recipient.
+ */
+class NewCallActivity : PassphraseRequiredActivity() {
companion object {
-
- private val TAG = Log.tag(NewCallActivity::class.java)
-
+ @JvmStatic
fun createIntent(context: Context): Intent {
return Intent(context, NewCallActivity::class.java)
- .putExtra(
- ContactSelectionArguments.DISPLAY_MODE,
- ContactSelectionDisplayMode.none()
- .withPush()
- .withActiveGroups()
- .withGroupMembers()
- .build()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState, ready)
+
+ val navigateBack = onBackPressedDispatcher::onBackPressed
+
+ setContent {
+ SignalTheme {
+ NewCallScreen(
+ closeScreen = navigateBack
)
- }
- }
-
- override fun onInvite() {
- startActivity(AppSettingsActivity.invite(this))
- }
-
- private fun handleManualRefresh() {
- if (!contactsFragment.isRefreshing) {
- contactsFragment.isRefreshing = true
- onRefresh()
- }
- }
-
- private inner class NewCallMenuProvider : MenuProvider {
- override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
- menuInflater.inflate(R.menu.new_call_menu, menu)
- }
-
- override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
- when (menuItem.itemId) {
- android.R.id.home -> ActivityCompat.finishAfterTransition(this@NewCallActivity)
- R.id.menu_refresh -> handleManualRefresh()
- R.id.menu_invite -> startActivity(AppSettingsActivity.invite(this@NewCallActivity))
}
-
- return true
}
}
}
+
+@Composable
+private fun NewCallScreen(
+ viewModel: NewCallViewModel = viewModel { NewCallViewModel() },
+ closeScreen: () -> Unit
+) {
+ val context = LocalContext.current as FragmentActivity
+
+ val callbacks = remember {
+ object : UiCallbacks {
+ override fun onSearchQueryChanged(query: String) = viewModel.onSearchQueryChanged(query)
+ override fun onRecipientSelected(selection: RecipientSelection) = viewModel.startCall(selection)
+ override fun onInviteToSignal() = context.startActivity(AppSettingsActivity.invite(context))
+ override fun onRefresh() = viewModel.refresh()
+ override fun onUserMessageDismissed(userMessage: UserMessage) = viewModel.clearUserMessage()
+ override fun onBackPressed() = closeScreen()
+ }
+ }
+
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ LaunchedEffect(uiState.pendingCall) {
+ val pendingCall = uiState.pendingCall ?: return@LaunchedEffect
+ when (pendingCall) {
+ is CallType.Video -> CommunicationActions.startVideoCall(context, pendingCall.recipient, viewModel::showUserAlreadyInACall)
+ is CallType.Voice -> CommunicationActions.startVoiceCall(context, pendingCall.recipient, viewModel::showUserAlreadyInACall)
+ }
+ viewModel.clearPendingCall()
+ }
+
+ NewCallScreenUi(
+ uiState = uiState,
+ callbacks = callbacks
+ )
+}
+
+private interface UiCallbacks :
+ RecipientPickerCallbacks.ListActions,
+ RecipientPickerCallbacks.Refresh,
+ RecipientPickerCallbacks.NewCall {
+
+ override suspend fun shouldAllowSelection(selection: RecipientSelection): Boolean = true
+ override fun onPendingRecipientSelectionsConsumed() = Unit
+ fun onUserMessageDismissed(userMessage: UserMessage)
+ fun onBackPressed()
+
+ object Empty : UiCallbacks {
+ override fun onSearchQueryChanged(query: String) = Unit
+ override fun onRecipientSelected(selection: RecipientSelection) = Unit
+ override fun onInviteToSignal() = Unit
+ override fun onRefresh() = Unit
+ override fun onUserMessageDismissed(userMessage: UserMessage) = Unit
+ override fun onBackPressed() = Unit
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class)
+@Composable
+private fun NewCallScreenUi(
+ uiState: NewCallUiState,
+ callbacks: UiCallbacks
+) {
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ RecipientPickerScaffold(
+ title = stringResource(R.string.NewCallActivity__new_call),
+ forceSplitPane = uiState.forceSplitPane,
+ onNavigateUpClick = callbacks::onBackPressed,
+ topAppBarActions = { TopAppBarActions(callbacks) },
+ snackbarHostState = snackbarHostState,
+ primaryContent = {
+ RecipientPicker(
+ searchQuery = uiState.searchQuery,
+ displayModes = setOf(RecipientPicker.DisplayMode.PUSH, RecipientPicker.DisplayMode.ACTIVE_GROUPS, RecipientPicker.DisplayMode.GROUP_MEMBERS),
+ isRefreshing = uiState.isRefreshingContacts,
+ callbacks = remember(callbacks) {
+ RecipientPickerCallbacks(
+ listActions = callbacks,
+ refresh = callbacks,
+ newCall = callbacks
+ )
+ },
+ modifier = Modifier.fillMaxSize()
+ )
+
+ UserMessagesHost(
+ userMessage = uiState.userMessage,
+ onDismiss = callbacks::onUserMessageDismissed,
+ snackbarHostState = snackbarHostState
+ )
+
+ if (uiState.isLookingUpRecipient) {
+ Dialogs.IndeterminateProgressDialog()
+ }
+ }
+ )
+}
+
+@Composable
+private fun TopAppBarActions(callbacks: UiCallbacks) {
+ val menuController = remember { DropdownMenus.MenuController() }
+ IconButton(
+ onClick = { menuController.show() },
+ modifier = Modifier.padding(horizontal = 8.dp)
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(R.drawable.symbol_more_vertical),
+ contentDescription = stringResource(R.string.NewConversationActivity__accessibility_open_top_bar_menu)
+ )
+ }
+
+ DropdownMenus.Menu(
+ controller = menuController,
+ offsetX = 24.dp,
+ modifier = Modifier
+ ) {
+ DropdownMenus.Item(
+ text = { Text(text = stringResource(R.string.new_conversation_activity__refresh)) },
+ onClick = {
+ callbacks.onRefresh()
+ menuController.hide()
+ }
+ )
+
+ DropdownMenus.Item(
+ text = { Text(text = stringResource(R.string.text_secure_normal__invite_friends)) },
+ onClick = {
+ callbacks.onInviteToSignal()
+ menuController.hide()
+ }
+ )
+ }
+}
+
+@Composable
+private fun UserMessagesHost(
+ userMessage: UserMessage?,
+ onDismiss: (UserMessage) -> Unit,
+ snackbarHostState: SnackbarHostState
+) {
+ val context = LocalContext.current
+
+ when (userMessage) {
+ null -> {}
+
+ is UserMessage.Info.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.Info.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) }
+ )
+
+ is UserMessage.Info.UserAlreadyInAnotherCall -> LaunchedEffect(userMessage) {
+ snackbarHostState.showSnackbar(
+ message = context.getString(R.string.CommunicationActions__you_are_already_in_a_call)
+ )
+ onDismiss(userMessage)
+ }
+ }
+}
+
+@AllDevicePreviews
+@Composable
+private fun NewCallScreenPreview() {
+ Previews.Preview {
+ NewCallScreenUi(
+ uiState = NewCallUiState(
+ forceSplitPane = false
+ ),
+ callbacks = UiCallbacks.Empty
+ )
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallViewModel.kt
new file mode 100644
index 0000000000..cb940973cd
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/calls/new/NewCallViewModel.kt
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2025 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.calls.new
+
+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.calls.new.NewCallUiState.CallType
+import org.thoughtcrime.securesms.calls.new.NewCallUiState.UserMessage.Info
+import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery
+import org.thoughtcrime.securesms.dependencies.AppDependencies
+import org.thoughtcrime.securesms.keyvalue.SignalStore
+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 NewCallViewModel : ViewModel() {
+ companion object {
+ private val TAG = Log.tag(NewCallViewModel::class)
+ }
+
+ private val internalUiState = MutableStateFlow(NewCallUiState())
+ val uiState: StateFlow = internalUiState.asStateFlow()
+
+ fun onSearchQueryChanged(query: String) {
+ internalUiState.update { it.copy(searchQuery = query) }
+ }
+
+ fun startCall(selection: RecipientSelection) {
+ viewModelScope.launch {
+ when (selection) {
+ is RecipientSelection.WithId -> resolveAndStartCall(selection.id)
+ is RecipientSelection.WithIdAndPhone -> resolveAndStartCall(selection.id)
+ is RecipientSelection.WithPhone -> {
+ Log.d(TAG, "[startCall] Missing recipientId: attempting to look up.")
+ resolveAndStartCall(selection.phone)
+ }
+ }
+ }
+ }
+
+ private suspend fun resolveAndStartCall(id: RecipientId) {
+ val recipient = withContext(Dispatchers.IO) {
+ Recipient.resolved(id)
+ }
+ openCall(recipient)
+ }
+
+ private suspend fun resolveAndStartCall(phone: PhoneNumber) {
+ if (!SignalStore.account.isRegistered) {
+ Log.w(TAG, "[resolveAndStartCall] Cannot look up recipient: account not registered.")
+ return
+ }
+ internalUiState.update { it.copy(isLookingUpRecipient = true) }
+
+ val lookupResult = withContext(Dispatchers.IO) {
+ RecipientRepository.lookupNewE164(inputE164 = phone.value)
+ }
+
+ when (lookupResult) {
+ is RecipientRepository.LookupResult.Success -> {
+ val recipient = withContext(Dispatchers.IO) {
+ Recipient.resolved(lookupResult.recipientId)
+ }
+ internalUiState.update { it.copy(isLookingUpRecipient = false) }
+ openCall(recipient)
+ }
+
+ is RecipientRepository.LookupResult.NotFound, is RecipientRepository.LookupResult.InvalidEntry -> {
+ internalUiState.update {
+ it.copy(
+ isLookingUpRecipient = false,
+ userMessage = Info.RecipientNotSignalUser(phone)
+ )
+ }
+ }
+
+ is RecipientRepository.LookupResult.NetworkError -> {
+ internalUiState.update {
+ it.copy(
+ isLookingUpRecipient = false,
+ userMessage = Info.NetworkError
+ )
+ }
+ }
+ }
+ }
+
+ private fun openCall(recipient: Recipient) {
+ if (!recipient.isRegistered && recipient.hasServiceId) {
+ Log.w(TAG, "[openCall] Unable to open call: recipient has a service ID but is not registered.")
+ return
+ }
+
+ internalUiState.update {
+ it.copy(
+ pendingCall = if (recipient.isGroup) {
+ CallType.Video(recipient)
+ } else {
+ CallType.Voice(recipient)
+ }
+ )
+ }
+ }
+
+ fun clearPendingCall() {
+ internalUiState.update { it.copy(pendingCall = null) }
+ }
+
+ fun showUserAlreadyInACall() {
+ internalUiState.update { it.copy(userMessage = Info.UserAlreadyInAnotherCall) }
+ }
+
+ fun refresh() {
+ if (internalUiState.value.isRefreshingContacts) {
+ return
+ }
+
+ viewModelScope.launch {
+ internalUiState.update { it.copy(isRefreshingContacts = true) }
+
+ withContext(Dispatchers.IO) {
+ ContactDiscovery.refreshAll(AppDependencies.application, true)
+ }
+
+ internalUiState.update { it.copy(isRefreshingContacts = false) }
+ }
+ }
+
+ fun clearUserMessage() {
+ internalUiState.update { it.copy(userMessage = null) }
+ }
+}
+
+data class NewCallUiState(
+ val forceSplitPane: Boolean = SignalStore.internal.forceSplitPane,
+ val searchQuery: String = "",
+ val isLookingUpRecipient: Boolean = false,
+ val isRefreshingContacts: Boolean = false,
+ val pendingCall: CallType? = null,
+ val userMessage: UserMessage? = null
+) {
+ sealed interface UserMessage {
+ sealed interface Info : UserMessage {
+ data class RecipientNotSignalUser(val phone: PhoneNumber) : Info
+ data object UserAlreadyInAnotherCall : Info
+ data object NetworkError : Info
+ }
+ }
+
+ sealed interface CallType {
+ data class Voice(val recipient: Recipient) : CallType
+ data class Video(val recipient: Recipient) : CallType
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/selection/ContactSelectionArguments.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/selection/ContactSelectionArguments.kt
index 8dde67a700..0bb5ff93b4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/contacts/selection/ContactSelectionArguments.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/selection/ContactSelectionArguments.kt
@@ -23,6 +23,7 @@ data class ContactSelectionArguments(
val currentSelection: Set = Defaults.CURRENT_SELECTION,
val canSelectSelf: Boolean = Defaults.canSelectSelf(selectionLimits),
val displayChips: Boolean = Defaults.DISPLAY_CHIPS,
+ val showCallButtons: Boolean = Defaults.SHOW_CALL_BUTTONS,
val recyclerPadBottom: Int = Defaults.RECYCLER_PADDING_BOTTOM,
val recyclerChildClipping: Boolean = Defaults.RECYCLER_CHILD_CLIPPING
) {
@@ -40,6 +41,7 @@ data class ContactSelectionArguments(
putParcelableArrayList(CURRENT_SELECTION, ArrayList(currentSelection))
putBoolean(CAN_SELECT_SELF, canSelectSelf)
putBoolean(DISPLAY_CHIPS, displayChips)
+ putBoolean(SHOW_CALL_BUTTONS, showCallButtons)
putInt(RV_PADDING_BOTTOM, recyclerPadBottom)
putBoolean(RV_CLIP, recyclerChildClipping)
}
@@ -57,6 +59,7 @@ data class ContactSelectionArguments(
const val CURRENT_SELECTION = "current_selection"
const val CAN_SELECT_SELF = "can_select_self"
const val DISPLAY_CHIPS = "display_chips"
+ const val SHOW_CALL_BUTTONS = "show_call_buttons"
const val RV_PADDING_BOTTOM = "recycler_view_padding_bottom"
const val RV_CLIP = "recycler_view_clipping"
@@ -81,6 +84,7 @@ data class ContactSelectionArguments(
currentSelection = currentSelection.toSet(),
canSelectSelf = bundle.getBoolean(CAN_SELECT_SELF, intent.getBooleanExtra(CAN_SELECT_SELF, Defaults.canSelectSelf(selectionLimits))),
displayChips = bundle.getBoolean(DISPLAY_CHIPS, intent.getBooleanExtra(DISPLAY_CHIPS, Defaults.DISPLAY_CHIPS)),
+ showCallButtons = bundle.getBoolean(SHOW_CALL_BUTTONS, intent.getBooleanExtra(SHOW_CALL_BUTTONS, Defaults.SHOW_CALL_BUTTONS)),
recyclerPadBottom = bundle.getInt(RV_PADDING_BOTTOM, intent.getIntExtra(RV_PADDING_BOTTOM, Defaults.RECYCLER_PADDING_BOTTOM)),
recyclerChildClipping = bundle.getBoolean(RV_CLIP, intent.getBooleanExtra(RV_CLIP, Defaults.RECYCLER_CHILD_CLIPPING))
)
@@ -98,6 +102,7 @@ data class ContactSelectionArguments(
val SELECTION_LIMITS: SelectionLimits? = null
val CURRENT_SELECTION: Set = emptySet()
const val DISPLAY_CHIPS = true
+ const val SHOW_CALL_BUTTONS = false
const val RECYCLER_PADDING_BOTTOM = -1
const val RECYCLER_CHILD_CLIPPING = true
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationActivity.kt
index c49a1ec870..5c0e09d638 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationActivity.kt
@@ -308,14 +308,16 @@ private fun NewConversationRecipientPicker(
searchQuery = uiState.searchQuery,
isRefreshing = uiState.isRefreshingContacts,
shouldResetContactsList = uiState.shouldResetContactsList,
- callbacks = RecipientPickerCallbacks(
- listActions = callbacks,
- refresh = callbacks,
- contextMenu = callbacks,
- newConversation = callbacks,
- findByUsername = callbacks,
- findByPhoneNumber = callbacks
- ),
+ callbacks = remember(callbacks) {
+ RecipientPickerCallbacks(
+ listActions = callbacks,
+ refresh = callbacks,
+ contextMenu = callbacks,
+ newConversation = callbacks,
+ findByUsername = callbacks,
+ findByPhoneNumber = callbacks
+ )
+ },
modifier = modifier.fillMaxSize()
)
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationViewModel.kt
index f5ed718600..9acfbc9370 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/NewConversationViewModel.kt
@@ -147,6 +147,10 @@ class NewConversationViewModel : ViewModel() {
}
fun refresh() {
+ if (internalUiState.value.isRefreshingContacts) {
+ return
+ }
+
viewModelScope.launch {
internalUiState.update { it.copy(isRefreshingContacts = true) }
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/RecipientPicker.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/RecipientPicker.kt
index 707f4ad413..da9c33970c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/RecipientPicker.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/RecipientPicker.kt
@@ -142,6 +142,7 @@ private fun RecipientSearchResultsList(
enableCreateNewGroup = callbacks.newConversation != null,
enableFindByUsername = callbacks.findByUsername != null,
enableFindByPhoneNumber = callbacks.findByPhoneNumber != null,
+ showCallButtons = callbacks.newCall != null,
selectionLimits = selectionLimits,
recyclerPadBottom = with(LocalDensity.current) { bottomPadding?.toPx()?.toInt() ?: ContactSelectionArguments.Defaults.RECYCLER_PADDING_BOTTOM },
recyclerChildClipping = clipListToPadding
@@ -228,6 +229,12 @@ private fun ContactSelectionListFragment.setUpCallbacks(
fragment.setNewConversationCallback(null)
}
+ if (callbacks.newCall != null) {
+ fragment.setNewCallCallback { callbacks.newCall.onInviteToSignal() }
+ } else {
+ fragment.setNewCallCallback(null)
+ }
+
if (callbacks.findByUsername != null || callbacks.findByPhoneNumber != null) {
fragment.setFindByCallback(object : ContactSelectionListFragment.FindByCallback {
override fun onFindByUsername() = callbacks.findByUsername?.onFindByUsername() ?: Unit
@@ -371,11 +378,12 @@ private fun RecipientPickerPreview() {
)
}
-data class RecipientPickerCallbacks(
+class RecipientPickerCallbacks(
val listActions: ListActions,
val refresh: Refresh? = null,
val contextMenu: ContextMenu? = null,
val newConversation: NewConversation? = null,
+ val newCall: NewCall? = null,
val findByUsername: FindByUsername? = null,
val findByPhoneNumber: FindByPhoneNumber? = null
) {
@@ -418,6 +426,10 @@ data class RecipientPickerCallbacks(
fun onInviteToSignal()
}
+ interface NewCall {
+ fun onInviteToSignal()
+ }
+
interface FindByUsername {
fun onFindByUsername()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.kt
index f967002892..88aed5adb2 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/CreateGroupActivity.kt
@@ -203,11 +203,13 @@ private fun CreateGroupRecipientPicker(
isRefreshing = false,
listBottomPadding = 64.dp,
clipListToPadding = false,
- callbacks = RecipientPickerCallbacks(
- listActions = callbacks,
- findByUsername = callbacks,
- findByPhoneNumber = callbacks
- ),
+ callbacks = remember(callbacks) {
+ RecipientPickerCallbacks(
+ listActions = callbacks,
+ findByUsername = callbacks,
+ findByPhoneNumber = callbacks
+ )
+ },
modifier = modifier.fillMaxSize()
)
diff --git a/app/src/main/res/menu/new_call_menu.xml b/app/src/main/res/menu/new_call_menu.xml
deleted file mode 100644
index feb43d2b9f..0000000000
--- a/app/src/main/res/menu/new_call_menu.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
\ No newline at end of file