mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-20 11:08:31 +00:00
Enable split pane UI for new call screen.
This commit is contained in:
committed by
Michelle Tang
parent
75346c3f6b
commit
a96a0a7009
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -147,6 +147,10 @@ class NewConversationViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
if (internalUiState.value.isRefreshingContacts) {
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
internalUiState.update { it.copy(isRefreshingContacts = true) }
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user