Enable split pane UI for new call screen.

This commit is contained in:
jeffrey-signal
2025-11-06 11:43:05 -05:00
committed by Michelle Tang
parent 75346c3f6b
commit a96a0a7009
10 changed files with 452 additions and 153 deletions

View File

@@ -713,11 +713,10 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".calls.new.NewCallActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity
android:name=".calls.new.NewCallActivity"
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar" />
<activity android:name=".PushContactSelectionActivity"
android:label="@string/AndroidManifest__select_contacts"

View File

@@ -154,7 +154,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
}
if (context instanceof NewCallCallback) {
newCallCallback = (NewCallCallback) context;
setNewCallCallback((NewCallCallback) context);
}
if (getParentFragment() instanceof ScrollCallback) {
@@ -206,6 +206,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
this.findByCallback = callback;
}
public void setNewCallCallback(@Nullable NewCallCallback callback) {
this.newCallCallback = callback;
}
public void setScrollCallback(@Nullable ScrollCallback callback) {
this.scrollCallback = callback;
}
@@ -340,7 +344,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
new ContactSearchAdapter.DisplayOptions(
isMulti,
ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
newCallCallback != null,
fragmentArgs.getShowCallButtons(),
false
),
this::mapStateToConfiguration,

View File

@@ -1,140 +1,253 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.calls.new
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.app.ActivityCompat
import androidx.core.view.MenuProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.concurrent.SimpleTask
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.ContactSelectionActivity
import org.thoughtcrime.securesms.ContactSelectionListFragment
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.SnackbarHostState
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.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.fragment.app.FragmentActivity
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
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.theme.SignalTheme
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.calls.new.NewCallUiState.CallType
import org.thoughtcrime.securesms.calls.new.NewCallUiState.UserMessage
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
import org.thoughtcrime.securesms.contacts.paged.ChatType
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientRepository
import org.thoughtcrime.securesms.conversation.RecipientPicker
import org.thoughtcrime.securesms.conversation.RecipientPickerCallbacks
import org.thoughtcrime.securesms.recipients.ui.RecipientPickerScaffold
import org.thoughtcrime.securesms.recipients.ui.RecipientSelection
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
import java.util.Optional
import java.util.function.Consumer
class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment.NewCallCallback {
override fun onCreate(icicle: Bundle?, ready: Boolean) {
super.onCreate(icicle, ready)
requireNotNull(supportActionBar)
supportActionBar?.setTitle(R.string.NewCallActivity__new_call)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
addMenuProvider(NewCallMenuProvider())
}
override fun onSelectionChanged() = Unit
override fun onBeforeContactSelected(isFromUnknownSearchKey: Boolean, recipientId: Optional<RecipientId?>, number: String?, chatType: Optional<ChatType>, callback: Consumer<Boolean?>) {
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
)
}
}
}
}
@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()
}
)
}
}
override fun onInvite() {
startActivity(AppSettingsActivity.invite(this))
}
@Composable
private fun UserMessagesHost(
userMessage: UserMessage?,
onDismiss: (UserMessage) -> Unit,
snackbarHostState: SnackbarHostState
) {
val context = LocalContext.current
private fun handleManualRefresh() {
if (!contactsFragment.isRefreshing) {
contactsFragment.isRefreshing = true
onRefresh()
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)
}
}
}
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
}
@AllDevicePreviews
@Composable
private fun NewCallScreenPreview() {
Previews.Preview {
NewCallScreenUi(
uiState = NewCallUiState(
forceSplitPane = false
),
callbacks = UiCallbacks.Empty
)
}
}

View File

@@ -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<NewCallUiState> = 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
}
}

View File

@@ -23,6 +23,7 @@ data class ContactSelectionArguments(
val currentSelection: Set<RecipientId> = 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<RecipientId> = emptySet()
const val DISPLAY_CHIPS = true
const val SHOW_CALL_BUTTONS = false
const val RECYCLER_PADDING_BOTTOM = -1
const val RECYCLER_CHILD_CLIPPING = true

View File

@@ -308,14 +308,16 @@ private fun NewConversationRecipientPicker(
searchQuery = uiState.searchQuery,
isRefreshing = uiState.isRefreshingContacts,
shouldResetContactsList = uiState.shouldResetContactsList,
callbacks = RecipientPickerCallbacks(
callbacks = remember(callbacks) {
RecipientPickerCallbacks(
listActions = callbacks,
refresh = callbacks,
contextMenu = callbacks,
newConversation = callbacks,
findByUsername = callbacks,
findByPhoneNumber = callbacks
),
)
},
modifier = modifier.fillMaxSize()
)
}

View File

@@ -147,6 +147,10 @@ class NewConversationViewModel : ViewModel() {
}
fun refresh() {
if (internalUiState.value.isRefreshingContacts) {
return
}
viewModelScope.launch {
internalUiState.update { it.copy(isRefreshingContacts = true) }

View File

@@ -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()
}

View File

@@ -203,11 +203,13 @@ private fun CreateGroupRecipientPicker(
isRefreshing = false,
listBottomPadding = 64.dp,
clipListToPadding = false,
callbacks = RecipientPickerCallbacks(
callbacks = remember(callbacks) {
RecipientPickerCallbacks(
listActions = callbacks,
findByUsername = callbacks,
findByPhoneNumber = callbacks
),
)
},
modifier = modifier.fillMaxSize()
)

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_refresh"
android:title="@string/new_conversation_activity__refresh" />
<item
android:id="@+id/menu_invite"
android:title="@string/text_secure_normal__invite_friends" />
</menu>