mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 17:29:32 +01:00
Create group v2 - Implement navigation to group details screen.
This commit is contained in:
@@ -159,7 +159,7 @@ public class CreateGroupActivity extends ContactSelectionActivity implements Con
|
||||
|
||||
@Override
|
||||
public void onSelectionChanged() {
|
||||
int selectedMembers = contactsFragment.getSelectedMembersSize();
|
||||
int selectedMembers = contactsFragment.getSelectedContactsCount();
|
||||
int selectedContactsCount = contactsFragment.getTotalMemberCount();
|
||||
if (selectedContactsCount == 0) {
|
||||
getToolbar().setTitle(getString(R.string.CreateGroupActivity__select_members));
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
|
||||
package org.thoughtcrime.securesms.groups.ui.creategroup
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.EnterTransition
|
||||
@@ -31,11 +34,13 @@ 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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
@@ -55,7 +60,9 @@ import org.thoughtcrime.securesms.contacts.SelectedContact
|
||||
import org.thoughtcrime.securesms.conversation.RecipientPicker
|
||||
import org.thoughtcrime.securesms.conversation.RecipientPickerCallbacks
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupUiState.NavTarget
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupUiState.UserMessage
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.details.AddGroupDetailsActivity
|
||||
import org.thoughtcrime.securesms.recipients.PhoneNumber
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity
|
||||
@@ -85,7 +92,10 @@ class CreateGroupActivityV2 : PassphraseRequiredActivity() {
|
||||
setContent {
|
||||
SignalTheme {
|
||||
CreateGroupScreen(
|
||||
closeScreen = navigateBack
|
||||
closeScreen = { resultCode ->
|
||||
resultCode?.let(::setResult)
|
||||
navigateBack()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -95,13 +105,22 @@ class CreateGroupActivityV2 : PassphraseRequiredActivity() {
|
||||
@Composable
|
||||
private fun CreateGroupScreen(
|
||||
viewModel: CreateGroupViewModel = viewModel { CreateGroupViewModel() },
|
||||
closeScreen: () -> Unit
|
||||
closeScreen: (resultCode: Int?) -> Unit
|
||||
) {
|
||||
val findByLauncher: ActivityResultLauncher<FindByMode> = rememberLauncherForActivityResult(
|
||||
contract = FindByActivity.Contract(),
|
||||
onResult = { id -> id?.let(viewModel::selectRecipient) }
|
||||
)
|
||||
|
||||
val addDetailsLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult(),
|
||||
onResult = { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
closeScreen(Activity.RESULT_OK)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val callbacks = remember {
|
||||
object : UiCallbacks {
|
||||
override fun onSearchQueryChanged(query: String) = viewModel.onSearchQueryChanged(query)
|
||||
@@ -112,11 +131,25 @@ private fun CreateGroupScreen(
|
||||
override fun onPendingRecipientSelectionsConsumed() = viewModel.clearPendingRecipientSelections()
|
||||
override fun onNextClicked(): Unit = viewModel.continueToGroupDetails()
|
||||
override fun onUserMessageDismissed(userMessage: UserMessage) = viewModel.clearUserMessage()
|
||||
override fun onBackPressed() = closeScreen()
|
||||
override fun onPendingDestinationConsumed() = viewModel.clearPendingDestination()
|
||||
override fun onBackPressed() = closeScreen(null)
|
||||
}
|
||||
}
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(uiState.pendingDestination) {
|
||||
when (val pendingDestination = uiState.pendingDestination) {
|
||||
is NavTarget.AddGroupDetails -> {
|
||||
addDetailsLauncher.launch(AddGroupDetailsActivity.newIntent(context, pendingDestination.recipientIds))
|
||||
callbacks.onPendingDestinationConsumed()
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
CreateGroupScreenUi(
|
||||
uiState = uiState,
|
||||
callbacks = callbacks
|
||||
@@ -265,6 +298,7 @@ private interface UiCallbacks :
|
||||
fun onNextClicked()
|
||||
fun onUserMessageDismissed(userMessage: UserMessage)
|
||||
fun onBackPressed()
|
||||
fun onPendingDestinationConsumed()
|
||||
|
||||
object Empty : UiCallbacks {
|
||||
override fun onSearchQueryChanged(query: String) = Unit
|
||||
@@ -275,6 +309,7 @@ private interface UiCallbacks :
|
||||
override fun onNextClicked() = Unit
|
||||
override fun onUserMessageDismissed(userMessage: UserMessage) = Unit
|
||||
override fun onBackPressed() = Unit
|
||||
override fun onPendingDestinationConsumed() = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,6 +318,7 @@ private fun UserMessagesHost(
|
||||
userMessage: UserMessage?,
|
||||
onDismiss: (UserMessage) -> Unit
|
||||
) {
|
||||
val context: Context = LocalContext.current
|
||||
when (userMessage) {
|
||||
null -> {}
|
||||
|
||||
@@ -297,6 +333,16 @@ private fun UserMessagesHost(
|
||||
dismiss = stringResource(android.R.string.ok),
|
||||
onDismiss = { onDismiss(userMessage) }
|
||||
)
|
||||
|
||||
is UserMessage.Info.RecipientsNotSignalUsers -> Dialogs.SimpleMessageDialog(
|
||||
message = pluralStringResource(
|
||||
id = R.plurals.CreateGroupActivity_not_signal_users,
|
||||
count = userMessage.recipients.size,
|
||||
userMessage.recipients.joinToString(", ") { it.getDisplayName(context) }
|
||||
),
|
||||
dismiss = stringResource(android.R.string.ok),
|
||||
onDismiss = { onDismiss(userMessage) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,22 +6,36 @@
|
||||
package org.thoughtcrime.securesms.groups.ui.creategroup
|
||||
|
||||
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.Stopwatch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.contacts.SelectedContact
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.groups.SelectionLimits
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupUiState.NavTarget
|
||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupUiState.UserMessage.Info
|
||||
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.util.RemoteConfig
|
||||
import java.io.IOException
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class CreateGroupViewModel : ViewModel() {
|
||||
companion object {
|
||||
private val TAG = Log.tag(CreateGroupViewModel::class)
|
||||
}
|
||||
|
||||
private val internalUiState = MutableStateFlow(CreateGroupUiState())
|
||||
val uiState: StateFlow<CreateGroupUiState> = internalUiState.asStateFlow()
|
||||
|
||||
@@ -91,12 +105,66 @@ class CreateGroupViewModel : ViewModel() {
|
||||
}
|
||||
|
||||
fun continueToGroupDetails() {
|
||||
// TODO [jeff] pass selected recipients to AddGroupDetailsActivity
|
||||
viewModelScope.launch {
|
||||
val stopwatch = Stopwatch(title = "Recipient Refresh")
|
||||
internalUiState.update { it.copy(isLookingUpRecipient = true) }
|
||||
|
||||
val selectedRecipients = uiState.value.newSelections.asRecipients(stopwatch)
|
||||
stopwatch.split(label = "registered")
|
||||
stopwatch.stop(tag = TAG)
|
||||
|
||||
val notSignalUsers = selectedRecipients.filter { !it.isRegistered || !it.hasServiceId }
|
||||
if (notSignalUsers.isNotEmpty()) {
|
||||
internalUiState.update {
|
||||
it.copy(
|
||||
isLookingUpRecipient = false,
|
||||
userMessage = Info.RecipientsNotSignalUsers(recipients = notSignalUsers)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
internalUiState.update {
|
||||
it.copy(
|
||||
isLookingUpRecipient = false,
|
||||
pendingDestination = NavTarget.AddGroupDetails(recipientIds = selectedRecipients.map(Recipient::id))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<SelectedContact>.asRecipients(stopwatch: Stopwatch): List<Recipient> {
|
||||
val selectedRecipientIds: List<RecipientId> = this.map { it.orCreateRecipientId }
|
||||
|
||||
val recipientsNeedingRegistrationCheck = Recipient
|
||||
.resolvedList(selectedRecipientIds)
|
||||
.also { stopwatch.split(label = "resolve") }
|
||||
.filter { !it.isRegistered || !it.hasServiceId }
|
||||
.toSet()
|
||||
|
||||
Log.d(TAG, "Need to do ${recipientsNeedingRegistrationCheck.size} registration checks.")
|
||||
recipientsNeedingRegistrationCheck.forEach { recipient ->
|
||||
try {
|
||||
ContactDiscovery.refresh(
|
||||
context = AppDependencies.application,
|
||||
recipient = recipient,
|
||||
notifyOfNewUsers = false,
|
||||
timeoutMs = 10.seconds.inWholeMilliseconds
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Failed to refresh registered status for ${recipient.id}", e)
|
||||
}
|
||||
}
|
||||
|
||||
return Recipient.resolvedList(selectedRecipientIds)
|
||||
}
|
||||
|
||||
fun clearUserMessage() {
|
||||
internalUiState.update { it.copy(userMessage = null) }
|
||||
}
|
||||
|
||||
fun clearPendingDestination() {
|
||||
internalUiState.update { it.copy(pendingDestination = null) }
|
||||
}
|
||||
}
|
||||
|
||||
data class CreateGroupUiState(
|
||||
@@ -107,12 +175,18 @@ data class CreateGroupUiState(
|
||||
val totalMembersCount: Int = 0,
|
||||
val isLookingUpRecipient: Boolean = false,
|
||||
val pendingRecipientSelections: Set<RecipientId> = emptySet(),
|
||||
val pendingDestination: NavTarget? = null,
|
||||
val userMessage: UserMessage? = null
|
||||
) {
|
||||
sealed interface UserMessage {
|
||||
sealed interface Info : UserMessage {
|
||||
data class RecipientNotSignalUser(val phone: PhoneNumber) : Info
|
||||
data class RecipientsNotSignalUsers(val recipients: List<Recipient>) : Info
|
||||
data object NetworkError : Info
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface NavTarget {
|
||||
data class AddGroupDetails(val recipientIds: List<RecipientId>) : NavTarget
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user