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: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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
listActions = callbacks,
|
RecipientPickerCallbacks(
|
||||||
refresh = callbacks,
|
listActions = callbacks,
|
||||||
contextMenu = callbacks,
|
refresh = callbacks,
|
||||||
newConversation = callbacks,
|
contextMenu = callbacks,
|
||||||
findByUsername = callbacks,
|
newConversation = callbacks,
|
||||||
findByPhoneNumber = callbacks
|
findByUsername = callbacks,
|
||||||
),
|
findByPhoneNumber = callbacks
|
||||||
|
)
|
||||||
|
},
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
listActions = callbacks,
|
RecipientPickerCallbacks(
|
||||||
findByUsername = callbacks,
|
listActions = callbacks,
|
||||||
findByPhoneNumber = callbacks
|
findByUsername = callbacks,
|
||||||
),
|
findByPhoneNumber = callbacks
|
||||||
|
)
|
||||||
|
},
|
||||||
modifier = modifier.fillMaxSize()
|
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