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

View File

@@ -154,7 +154,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
} }
if (context instanceof NewCallCallback) { if (context instanceof NewCallCallback) {
newCallCallback = (NewCallCallback) context; setNewCallCallback((NewCallCallback) context);
} }
if (getParentFragment() instanceof ScrollCallback) { if (getParentFragment() instanceof ScrollCallback) {
@@ -206,6 +206,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
this.findByCallback = callback; this.findByCallback = callback;
} }
public void setNewCallCallback(@Nullable NewCallCallback callback) {
this.newCallCallback = callback;
}
public void setScrollCallback(@Nullable ScrollCallback callback) { public void setScrollCallback(@Nullable ScrollCallback callback) {
this.scrollCallback = callback; this.scrollCallback = callback;
} }
@@ -340,7 +344,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
new ContactSearchAdapter.DisplayOptions( new ContactSearchAdapter.DisplayOptions(
isMulti, isMulti,
ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS, ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
newCallCallback != null, fragmentArgs.getShowCallButtons(),
false false
), ),
this::mapStateToConfiguration, 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 package org.thoughtcrime.securesms.calls.new
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import androidx.activity.compose.setContent
import android.view.MenuInflater import androidx.activity.enableEdgeToEdge
import android.view.MenuItem import androidx.compose.foundation.layout.fillMaxSize
import androidx.core.app.ActivityCompat import androidx.compose.foundation.layout.padding
import androidx.core.view.MenuProvider import androidx.compose.material3.ExperimentalMaterial3Api
import com.google.android.material.dialog.MaterialAlertDialogBuilder import androidx.compose.material3.Icon
import org.signal.core.util.concurrent.SimpleTask import androidx.compose.material3.IconButton
import org.signal.core.util.logging.Log import androidx.compose.material3.SnackbarHostState
import org.thoughtcrime.securesms.ContactSelectionActivity import androidx.compose.material3.Text
import org.thoughtcrime.securesms.ContactSelectionListFragment 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.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.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode import org.thoughtcrime.securesms.conversation.RecipientPicker
import org.thoughtcrime.securesms.contacts.paged.ChatType import org.thoughtcrime.securesms.conversation.RecipientPickerCallbacks
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments import org.thoughtcrime.securesms.recipients.ui.RecipientPickerScaffold
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.ui.RecipientSelection
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientRepository
import org.thoughtcrime.securesms.util.CommunicationActions 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 { companion object {
@JvmStatic
private val TAG = Log.tag(NewCallActivity::class.java)
fun createIntent(context: Context): Intent { fun createIntent(context: Context): Intent {
return Intent(context, NewCallActivity::class.java) return Intent(context, NewCallActivity::class.java)
.putExtra( }
ContactSelectionArguments.DISPLAY_MODE, }
ContactSelectionDisplayMode.none()
.withPush() override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
.withActiveGroups() enableEdgeToEdge()
.withGroupMembers() super.onCreate(savedInstanceState, ready)
.build()
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() { @Composable
startActivity(AppSettingsActivity.invite(this)) private fun UserMessagesHost(
} userMessage: UserMessage?,
onDismiss: (UserMessage) -> Unit,
snackbarHostState: SnackbarHostState
) {
val context = LocalContext.current
private fun handleManualRefresh() { when (userMessage) {
if (!contactsFragment.isRefreshing) { null -> {}
contactsFragment.isRefreshing = true
onRefresh() 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 { @AllDevicePreviews
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { @Composable
menuInflater.inflate(R.menu.new_call_menu, menu) private fun NewCallScreenPreview() {
} Previews.Preview {
NewCallScreenUi(
override fun onMenuItemSelected(menuItem: MenuItem): Boolean { uiState = NewCallUiState(
when (menuItem.itemId) { forceSplitPane = false
android.R.id.home -> ActivityCompat.finishAfterTransition(this@NewCallActivity) ),
R.id.menu_refresh -> handleManualRefresh() callbacks = UiCallbacks.Empty
R.id.menu_invite -> startActivity(AppSettingsActivity.invite(this@NewCallActivity)) )
}
return true
}
} }
} }

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 currentSelection: Set<RecipientId> = Defaults.CURRENT_SELECTION,
val canSelectSelf: Boolean = Defaults.canSelectSelf(selectionLimits), val canSelectSelf: Boolean = Defaults.canSelectSelf(selectionLimits),
val displayChips: Boolean = Defaults.DISPLAY_CHIPS, val displayChips: Boolean = Defaults.DISPLAY_CHIPS,
val showCallButtons: Boolean = Defaults.SHOW_CALL_BUTTONS,
val recyclerPadBottom: Int = Defaults.RECYCLER_PADDING_BOTTOM, val recyclerPadBottom: Int = Defaults.RECYCLER_PADDING_BOTTOM,
val recyclerChildClipping: Boolean = Defaults.RECYCLER_CHILD_CLIPPING val recyclerChildClipping: Boolean = Defaults.RECYCLER_CHILD_CLIPPING
) { ) {
@@ -40,6 +41,7 @@ data class ContactSelectionArguments(
putParcelableArrayList(CURRENT_SELECTION, ArrayList(currentSelection)) putParcelableArrayList(CURRENT_SELECTION, ArrayList(currentSelection))
putBoolean(CAN_SELECT_SELF, canSelectSelf) putBoolean(CAN_SELECT_SELF, canSelectSelf)
putBoolean(DISPLAY_CHIPS, displayChips) putBoolean(DISPLAY_CHIPS, displayChips)
putBoolean(SHOW_CALL_BUTTONS, showCallButtons)
putInt(RV_PADDING_BOTTOM, recyclerPadBottom) putInt(RV_PADDING_BOTTOM, recyclerPadBottom)
putBoolean(RV_CLIP, recyclerChildClipping) putBoolean(RV_CLIP, recyclerChildClipping)
} }
@@ -57,6 +59,7 @@ data class ContactSelectionArguments(
const val CURRENT_SELECTION = "current_selection" const val CURRENT_SELECTION = "current_selection"
const val CAN_SELECT_SELF = "can_select_self" const val CAN_SELECT_SELF = "can_select_self"
const val DISPLAY_CHIPS = "display_chips" 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_PADDING_BOTTOM = "recycler_view_padding_bottom"
const val RV_CLIP = "recycler_view_clipping" const val RV_CLIP = "recycler_view_clipping"
@@ -81,6 +84,7 @@ data class ContactSelectionArguments(
currentSelection = currentSelection.toSet(), currentSelection = currentSelection.toSet(),
canSelectSelf = bundle.getBoolean(CAN_SELECT_SELF, intent.getBooleanExtra(CAN_SELECT_SELF, Defaults.canSelectSelf(selectionLimits))), 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)), 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)), 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)) 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 SELECTION_LIMITS: SelectionLimits? = null
val CURRENT_SELECTION: Set<RecipientId> = emptySet() val CURRENT_SELECTION: Set<RecipientId> = emptySet()
const val DISPLAY_CHIPS = true const val DISPLAY_CHIPS = true
const val SHOW_CALL_BUTTONS = false
const val RECYCLER_PADDING_BOTTOM = -1 const val RECYCLER_PADDING_BOTTOM = -1
const val RECYCLER_CHILD_CLIPPING = true const val RECYCLER_CHILD_CLIPPING = true

View File

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

View File

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

View File

@@ -142,6 +142,7 @@ private fun RecipientSearchResultsList(
enableCreateNewGroup = callbacks.newConversation != null, enableCreateNewGroup = callbacks.newConversation != null,
enableFindByUsername = callbacks.findByUsername != null, enableFindByUsername = callbacks.findByUsername != null,
enableFindByPhoneNumber = callbacks.findByPhoneNumber != null, enableFindByPhoneNumber = callbacks.findByPhoneNumber != null,
showCallButtons = callbacks.newCall != null,
selectionLimits = selectionLimits, selectionLimits = selectionLimits,
recyclerPadBottom = with(LocalDensity.current) { bottomPadding?.toPx()?.toInt() ?: ContactSelectionArguments.Defaults.RECYCLER_PADDING_BOTTOM }, recyclerPadBottom = with(LocalDensity.current) { bottomPadding?.toPx()?.toInt() ?: ContactSelectionArguments.Defaults.RECYCLER_PADDING_BOTTOM },
recyclerChildClipping = clipListToPadding recyclerChildClipping = clipListToPadding
@@ -228,6 +229,12 @@ private fun ContactSelectionListFragment.setUpCallbacks(
fragment.setNewConversationCallback(null) fragment.setNewConversationCallback(null)
} }
if (callbacks.newCall != null) {
fragment.setNewCallCallback { callbacks.newCall.onInviteToSignal() }
} else {
fragment.setNewCallCallback(null)
}
if (callbacks.findByUsername != null || callbacks.findByPhoneNumber != null) { if (callbacks.findByUsername != null || callbacks.findByPhoneNumber != null) {
fragment.setFindByCallback(object : ContactSelectionListFragment.FindByCallback { fragment.setFindByCallback(object : ContactSelectionListFragment.FindByCallback {
override fun onFindByUsername() = callbacks.findByUsername?.onFindByUsername() ?: Unit override fun onFindByUsername() = callbacks.findByUsername?.onFindByUsername() ?: Unit
@@ -371,11 +378,12 @@ private fun RecipientPickerPreview() {
) )
} }
data class RecipientPickerCallbacks( class RecipientPickerCallbacks(
val listActions: ListActions, val listActions: ListActions,
val refresh: Refresh? = null, val refresh: Refresh? = null,
val contextMenu: ContextMenu? = null, val contextMenu: ContextMenu? = null,
val newConversation: NewConversation? = null, val newConversation: NewConversation? = null,
val newCall: NewCall? = null,
val findByUsername: FindByUsername? = null, val findByUsername: FindByUsername? = null,
val findByPhoneNumber: FindByPhoneNumber? = null val findByPhoneNumber: FindByPhoneNumber? = null
) { ) {
@@ -418,6 +426,10 @@ data class RecipientPickerCallbacks(
fun onInviteToSignal() fun onInviteToSignal()
} }
interface NewCall {
fun onInviteToSignal()
}
interface FindByUsername { interface FindByUsername {
fun onFindByUsername() fun onFindByUsername()
} }

View File

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