mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-24 10:51:27 +01:00
New conversation v2 - Implement remaining functionality.
This commit is contained in:
committed by
Cody Henthorne
parent
802f980c6f
commit
4fd4792dd8
@@ -158,11 +158,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (getParentFragment() instanceof ScrollCallback) {
|
if (getParentFragment() instanceof ScrollCallback) {
|
||||||
scrollCallback = (ScrollCallback) getParentFragment();
|
setScrollCallback((ScrollCallback) getParentFragment());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context instanceof ScrollCallback) {
|
if (context instanceof ScrollCallback) {
|
||||||
scrollCallback = (ScrollCallback) context;
|
setScrollCallback((ScrollCallback) context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getParentFragment() instanceof OnContactSelectedListener) {
|
if (getParentFragment() instanceof OnContactSelectedListener) {
|
||||||
@@ -190,11 +190,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (context instanceof OnItemLongClickListener) {
|
if (context instanceof OnItemLongClickListener) {
|
||||||
onItemLongClickListener = (OnItemLongClickListener) context;
|
setOnItemLongClickListener((OnItemLongClickListener) context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getParentFragment() instanceof OnItemLongClickListener) {
|
if (getParentFragment() instanceof OnItemLongClickListener) {
|
||||||
onItemLongClickListener = (OnItemLongClickListener) getParentFragment();
|
setOnItemLongClickListener((OnItemLongClickListener) getParentFragment());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,10 +206,18 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
|||||||
this.findByCallback = callback;
|
this.findByCallback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setScrollCallback(@Nullable ScrollCallback callback) {
|
||||||
|
this.scrollCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
public void setOnContactSelectedListener(@Nullable OnContactSelectedListener listener) {
|
public void setOnContactSelectedListener(@Nullable OnContactSelectedListener listener) {
|
||||||
this.onContactSelectedListener = listener;
|
this.onContactSelectedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOnItemLongClickListener(@Nullable OnItemLongClickListener listener) {
|
||||||
|
this.onItemLongClickListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityCreated(Bundle icicle) {
|
public void onActivityCreated(Bundle icicle) {
|
||||||
super.onActivityCreated(icicle);
|
super.onActivityCreated(icicle);
|
||||||
|
|||||||
@@ -18,13 +18,18 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
@@ -32,14 +37,19 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
import org.signal.core.ui.compose.AllDevicePreviews
|
import org.signal.core.ui.compose.AllDevicePreviews
|
||||||
import org.signal.core.ui.compose.Dialogs
|
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.Previews
|
||||||
import org.signal.core.ui.compose.Scaffolds
|
import org.signal.core.ui.compose.Scaffolds
|
||||||
import org.signal.core.ui.compose.theme.SignalTheme
|
import org.signal.core.ui.compose.theme.SignalTheme
|
||||||
|
import org.thoughtcrime.securesms.BlockUnblockDialog
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.compose.ScreenTitlePane
|
import org.thoughtcrime.securesms.components.compose.ScreenTitlePane
|
||||||
@@ -47,9 +57,11 @@ import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
|
|||||||
import org.thoughtcrime.securesms.conversation.NewConversationUiState.UserMessage
|
import org.thoughtcrime.securesms.conversation.NewConversationUiState.UserMessage
|
||||||
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
|
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
|
||||||
import org.thoughtcrime.securesms.recipients.PhoneNumber
|
import org.thoughtcrime.securesms.recipients.PhoneNumber
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity
|
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity
|
||||||
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode
|
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode
|
||||||
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
import org.thoughtcrime.securesms.window.AppScaffold
|
import org.thoughtcrime.securesms.window.AppScaffold
|
||||||
import org.thoughtcrime.securesms.window.WindowSizeClass
|
import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||||
import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
|
import org.thoughtcrime.securesms.window.rememberAppScaffoldNavigator
|
||||||
@@ -87,7 +99,7 @@ private fun NewConversationScreen(
|
|||||||
activityIntent: Intent,
|
activityIntent: Intent,
|
||||||
closeScreen: () -> Unit
|
closeScreen: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current as FragmentActivity
|
||||||
|
|
||||||
val createGroupLauncher: ActivityResultLauncher<Intent> = rememberLauncherForActivityResult(
|
val createGroupLauncher: ActivityResultLauncher<Intent> = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult(),
|
contract = ActivityResultContracts.StartActivityForResult(),
|
||||||
@@ -107,6 +119,7 @@ private fun NewConversationScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val callbacks = remember {
|
val callbacks = remember {
|
||||||
object : Callbacks {
|
object : Callbacks {
|
||||||
override fun onCreateNewGroup() = createGroupLauncher.launch(CreateGroupActivity.newIntent(context))
|
override fun onCreateNewGroup() = createGroupLauncher.launch(CreateGroupActivity.newIntent(context))
|
||||||
@@ -115,8 +128,23 @@ private fun NewConversationScreen(
|
|||||||
override fun shouldAllowSelection(id: RecipientId): Boolean = true
|
override fun shouldAllowSelection(id: RecipientId): Boolean = true
|
||||||
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = viewModel.onRecipientSelected(id, phone)
|
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = viewModel.onRecipientSelected(id, phone)
|
||||||
override fun onMessage(id: RecipientId) = viewModel.onMessage(id)
|
override fun onMessage(id: RecipientId) = viewModel.onMessage(id)
|
||||||
|
override fun onVoiceCall(recipient: Recipient) = CommunicationActions.startVoiceCall(context, recipient, viewModel::onUserAlreadyInACall)
|
||||||
|
override fun onVideoCall(recipient: Recipient) = CommunicationActions.startVideoCall(context, recipient, viewModel::onUserAlreadyInACall)
|
||||||
|
|
||||||
|
override fun onRemove(recipient: Recipient) = viewModel.showRemoveConfirmation(recipient)
|
||||||
|
override fun onRemoveConfirmed(recipient: Recipient) {
|
||||||
|
coroutineScope.launch { viewModel.removeRecipient(recipient) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBlock(recipient: Recipient) = viewModel.showBlockConfirmation(recipient)
|
||||||
|
override fun onBlockConfirmed(recipient: Recipient) {
|
||||||
|
coroutineScope.launch { viewModel.blockRecipient(recipient) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onInviteToSignal() = context.startActivity(AppSettingsActivity.invite(context))
|
override fun onInviteToSignal() = context.startActivity(AppSettingsActivity.invite(context))
|
||||||
|
override fun onRefresh() = viewModel.refresh()
|
||||||
override fun onUserMessageDismissed(userMessage: UserMessage) = viewModel.onUserMessageDismissed()
|
override fun onUserMessageDismissed(userMessage: UserMessage) = viewModel.onUserMessageDismissed()
|
||||||
|
override fun onContactsListReset() = viewModel.onContactsListReset()
|
||||||
override fun onBackPressed() = closeScreen()
|
override fun onBackPressed() = closeScreen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +195,7 @@ private fun NewConversationScreenUi(
|
|||||||
) {
|
) {
|
||||||
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
|
||||||
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPaneOnCompactLandscape = uiState.forceSplitPaneOnCompactLandscape)
|
val isSplitPane = windowSizeClass.isSplitPane(forceSplitPaneOnCompactLandscape = uiState.forceSplitPaneOnCompactLandscape)
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
topBarContent = {
|
topBarContent = {
|
||||||
@@ -175,7 +204,8 @@ private fun NewConversationScreenUi(
|
|||||||
titleContent = { _, title -> Text(text = title, style = MaterialTheme.typography.titleLarge) },
|
titleContent = { _, title -> Text(text = title, style = MaterialTheme.typography.titleLarge) },
|
||||||
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
|
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
|
||||||
navigationContentDescription = stringResource(R.string.DefaultTopAppBar__navigate_up_content_description),
|
navigationContentDescription = stringResource(R.string.DefaultTopAppBar__navigate_up_content_description),
|
||||||
onNavigationClick = callbacks::onBackPressed
|
onNavigationClick = callbacks::onBackPressed,
|
||||||
|
actions = { TopAppBarActions(callbacks) }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -187,6 +217,7 @@ private fun NewConversationScreenUi(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
NewConversationRecipientPicker(
|
NewConversationRecipientPicker(
|
||||||
|
uiState = uiState,
|
||||||
callbacks = callbacks
|
callbacks = callbacks
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -198,12 +229,17 @@ private fun NewConversationScreenUi(
|
|||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
NewConversationRecipientPicker(
|
NewConversationRecipientPicker(
|
||||||
|
uiState = uiState,
|
||||||
callbacks = callbacks,
|
callbacks = callbacks,
|
||||||
modifier = Modifier.widthIn(max = windowSizeClass.detailPaneMaxContentWidth)
|
modifier = Modifier.widthIn(max = windowSizeClass.detailPaneMaxContentWidth)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
snackbarHost = {
|
||||||
|
SnackbarHost(snackbarHostState)
|
||||||
|
},
|
||||||
|
|
||||||
navigator = rememberAppScaffoldNavigator(
|
navigator = rememberAppScaffoldNavigator(
|
||||||
isSplitPane = isSplitPane
|
isSplitPane = isSplitPane
|
||||||
)
|
)
|
||||||
@@ -211,7 +247,10 @@ private fun NewConversationScreenUi(
|
|||||||
|
|
||||||
UserMessagesHost(
|
UserMessagesHost(
|
||||||
userMessage = uiState.userMessage,
|
userMessage = uiState.userMessage,
|
||||||
onDismiss = callbacks::onUserMessageDismissed
|
onDismiss = callbacks::onUserMessageDismissed,
|
||||||
|
onRemoveConfirmed = callbacks::onRemoveConfirmed,
|
||||||
|
onBlockConfirmed = callbacks::onBlockConfirmed,
|
||||||
|
snackbarHostState = snackbarHostState
|
||||||
)
|
)
|
||||||
|
|
||||||
if (uiState.isRefreshingRecipient) {
|
if (uiState.isRefreshingRecipient) {
|
||||||
@@ -219,7 +258,54 @@ private fun NewConversationScreenUi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun TopAppBarActions(callbacks: Callbacks) {
|
||||||
|
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,
|
||||||
|
offsetY = 0.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__menu_new_group)) },
|
||||||
|
onClick = {
|
||||||
|
callbacks.onCreateNewGroup()
|
||||||
|
menuController.hide()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenus.Item(
|
||||||
|
text = { Text(text = stringResource(R.string.text_secure_normal__invite_friends)) },
|
||||||
|
onClick = {
|
||||||
|
callbacks.onInviteToSignal()
|
||||||
|
menuController.hide()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private interface Callbacks : RecipientPickerCallbacks {
|
private interface Callbacks : RecipientPickerCallbacks {
|
||||||
|
fun onRemoveConfirmed(recipient: Recipient)
|
||||||
|
fun onBlockConfirmed(recipient: Recipient)
|
||||||
fun onUserMessageDismissed(userMessage: UserMessage)
|
fun onUserMessageDismissed(userMessage: UserMessage)
|
||||||
fun onBackPressed()
|
fun onBackPressed()
|
||||||
|
|
||||||
@@ -230,7 +316,15 @@ private interface Callbacks : RecipientPickerCallbacks {
|
|||||||
override fun shouldAllowSelection(id: RecipientId): Boolean = true
|
override fun shouldAllowSelection(id: RecipientId): Boolean = true
|
||||||
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = Unit
|
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = Unit
|
||||||
override fun onMessage(id: RecipientId) = Unit
|
override fun onMessage(id: RecipientId) = Unit
|
||||||
|
override fun onVoiceCall(recipient: Recipient) = Unit
|
||||||
|
override fun onVideoCall(recipient: Recipient) = Unit
|
||||||
|
override fun onRemove(recipient: Recipient) = Unit
|
||||||
|
override fun onRemoveConfirmed(recipient: Recipient) = Unit
|
||||||
|
override fun onBlock(recipient: Recipient) = Unit
|
||||||
|
override fun onBlockConfirmed(recipient: Recipient) = Unit
|
||||||
override fun onInviteToSignal() = Unit
|
override fun onInviteToSignal() = Unit
|
||||||
|
override fun onRefresh() = Unit
|
||||||
|
override fun onContactsListReset() = Unit
|
||||||
override fun onUserMessageDismissed(userMessage: UserMessage) = Unit
|
override fun onUserMessageDismissed(userMessage: UserMessage) = Unit
|
||||||
override fun onBackPressed() = Unit
|
override fun onBackPressed() = Unit
|
||||||
}
|
}
|
||||||
@@ -238,6 +332,7 @@ private interface Callbacks : RecipientPickerCallbacks {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun NewConversationRecipientPicker(
|
private fun NewConversationRecipientPicker(
|
||||||
|
uiState: NewConversationUiState,
|
||||||
callbacks: Callbacks,
|
callbacks: Callbacks,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -245,6 +340,8 @@ private fun NewConversationRecipientPicker(
|
|||||||
enableCreateNewGroup = true,
|
enableCreateNewGroup = true,
|
||||||
enableFindByUsername = true,
|
enableFindByUsername = true,
|
||||||
enableFindByPhoneNumber = true,
|
enableFindByPhoneNumber = true,
|
||||||
|
isRefreshing = uiState.isRefreshingContacts,
|
||||||
|
shouldResetContactsList = uiState.shouldResetContactsList,
|
||||||
callbacks = callbacks,
|
callbacks = callbacks,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -255,22 +352,66 @@ private fun NewConversationRecipientPicker(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun UserMessagesHost(
|
private fun UserMessagesHost(
|
||||||
userMessage: UserMessage?,
|
userMessage: UserMessage?,
|
||||||
onDismiss: (UserMessage) -> Unit
|
onDismiss: (UserMessage) -> Unit,
|
||||||
|
onBlockConfirmed: (Recipient) -> Unit,
|
||||||
|
onRemoveConfirmed: (Recipient) -> Unit,
|
||||||
|
snackbarHostState: SnackbarHostState
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
when (userMessage) {
|
when (userMessage) {
|
||||||
null -> {}
|
null -> {}
|
||||||
|
|
||||||
UserMessage.NetworkError -> Dialogs.SimpleMessageDialog(
|
is UserMessage.Info.RecipientRemoved -> LaunchedEffect(userMessage) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = context.getString(R.string.NewConversationActivity__s_has_been_removed, userMessage.recipient.getDisplayName(context))
|
||||||
|
)
|
||||||
|
onDismiss(userMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
is UserMessage.Info.RecipientBlocked -> LaunchedEffect(userMessage) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = context.getString(R.string.NewConversationActivity__s_has_been_blocked, userMessage.recipient.getDisplayName(context))
|
||||||
|
)
|
||||||
|
onDismiss(userMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
is UserMessage.Info.NetworkError -> Dialogs.SimpleMessageDialog(
|
||||||
message = stringResource(R.string.NetworkFailure__network_error_check_your_connection_and_try_again),
|
message = stringResource(R.string.NetworkFailure__network_error_check_your_connection_and_try_again),
|
||||||
dismiss = stringResource(android.R.string.ok),
|
dismiss = stringResource(android.R.string.ok),
|
||||||
onDismiss = { onDismiss(userMessage) }
|
onDismiss = { onDismiss(userMessage) }
|
||||||
)
|
)
|
||||||
|
|
||||||
is UserMessage.RecipientNotSignalUser -> Dialogs.SimpleMessageDialog(
|
is UserMessage.Info.RecipientNotSignalUser -> Dialogs.SimpleMessageDialog(
|
||||||
message = stringResource(R.string.NewConversationActivity__s_is_not_a_signal_user, userMessage.phone!!.displayText),
|
message = stringResource(R.string.NewConversationActivity__s_is_not_a_signal_user, userMessage.phone!!.displayText),
|
||||||
dismiss = stringResource(android.R.string.ok),
|
dismiss = stringResource(android.R.string.ok),
|
||||||
onDismiss = { onDismiss(userMessage) }
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
is UserMessage.Prompt.ConfirmRemoveRecipient -> Dialogs.SimpleAlertDialog(
|
||||||
|
title = stringResource(R.string.NewConversationActivity__remove_s, userMessage.recipient.getShortDisplayName(context)),
|
||||||
|
body = stringResource(R.string.NewConversationActivity__you_wont_see_this_person),
|
||||||
|
confirm = stringResource(R.string.NewConversationActivity__remove),
|
||||||
|
dismiss = stringResource(android.R.string.cancel),
|
||||||
|
onConfirm = { onRemoveConfirmed(userMessage.recipient) },
|
||||||
|
onDismiss = { onDismiss(userMessage) }
|
||||||
|
)
|
||||||
|
|
||||||
|
is UserMessage.Prompt.ConfirmBlockRecipient -> {
|
||||||
|
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
||||||
|
LaunchedEffect(userMessage.recipient) {
|
||||||
|
BlockUnblockDialog.showBlockFor(context, lifecycle, userMessage.recipient) {
|
||||||
|
onBlockConfirmed(userMessage.recipient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,17 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.rx3.await
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.thoughtcrime.securesms.conversation.NewConversationUiState.UserMessage
|
import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository
|
||||||
|
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery
|
||||||
|
import org.thoughtcrime.securesms.conversation.NewConversationUiState.UserMessage.Info
|
||||||
|
import org.thoughtcrime.securesms.conversation.NewConversationUiState.UserMessage.Prompt
|
||||||
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.recipients.PhoneNumber
|
import org.thoughtcrime.securesms.recipients.PhoneNumber
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient.Companion.resolved
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientRepository
|
import org.thoughtcrime.securesms.recipients.RecipientRepository
|
||||||
|
|
||||||
@@ -30,6 +35,8 @@ class NewConversationViewModel : ViewModel() {
|
|||||||
private val _uiState = MutableStateFlow(NewConversationUiState())
|
private val _uiState = MutableStateFlow(NewConversationUiState())
|
||||||
val uiState: StateFlow<NewConversationUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<NewConversationUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
private val contactsManagementRepo = ContactsManagementRepository(AppDependencies.application)
|
||||||
|
|
||||||
fun onMessage(id: RecipientId): Unit = openConversation(recipientId = id)
|
fun onMessage(id: RecipientId): Unit = openConversation(recipientId = id)
|
||||||
|
|
||||||
fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) {
|
fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) {
|
||||||
@@ -63,7 +70,7 @@ class NewConversationViewModel : ViewModel() {
|
|||||||
|
|
||||||
when (lookupResult) {
|
when (lookupResult) {
|
||||||
is RecipientRepository.LookupResult.Success -> {
|
is RecipientRepository.LookupResult.Success -> {
|
||||||
val recipient = resolved(lookupResult.recipientId)
|
val recipient = Recipient.resolved(lookupResult.recipientId)
|
||||||
_uiState.update { it.copy(isRefreshingRecipient = false) }
|
_uiState.update { it.copy(isRefreshingRecipient = false) }
|
||||||
|
|
||||||
if (recipient.isRegistered && recipient.hasServiceId) {
|
if (recipient.isRegistered && recipient.hasServiceId) {
|
||||||
@@ -77,7 +84,7 @@ class NewConversationViewModel : ViewModel() {
|
|||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isRefreshingRecipient = false,
|
isRefreshingRecipient = false,
|
||||||
userMessage = UserMessage.RecipientNotSignalUser(phone)
|
userMessage = Info.RecipientNotSignalUser(phone)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +93,7 @@ class NewConversationViewModel : ViewModel() {
|
|||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isRefreshingRecipient = false,
|
isRefreshingRecipient = false,
|
||||||
userMessage = UserMessage.NetworkError
|
userMessage = Info.NetworkError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,6 +101,62 @@ class NewConversationViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showRemoveConfirmation(recipient: Recipient) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(userMessage = Prompt.ConfirmRemoveRecipient(recipient))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun removeRecipient(recipient: Recipient) {
|
||||||
|
contactsManagementRepo.hideContact(recipient).await()
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
shouldResetContactsList = true,
|
||||||
|
userMessage = Info.RecipientRemoved(recipient)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showBlockConfirmation(recipient: Recipient) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(userMessage = Prompt.ConfirmBlockRecipient(recipient))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun blockRecipient(recipient: Recipient) {
|
||||||
|
contactsManagementRepo.blockContact(recipient).await()
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
shouldResetContactsList = true,
|
||||||
|
userMessage = Info.RecipientBlocked(recipient)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onUserAlreadyInACall() {
|
||||||
|
_uiState.update { it.copy(userMessage = Info.UserAlreadyInAnotherCall) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onContactsListReset() {
|
||||||
|
_uiState.update { it.copy(shouldResetContactsList = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(isRefreshingContacts = true) }
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
ContactDiscovery.refreshAll(AppDependencies.application, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiState.update { it.copy(isRefreshingContacts = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun onUserMessageDismissed() {
|
fun onUserMessageDismissed() {
|
||||||
_uiState.update { it.copy(userMessage = null) }
|
_uiState.update { it.copy(userMessage = null) }
|
||||||
}
|
}
|
||||||
@@ -102,11 +165,23 @@ class NewConversationViewModel : ViewModel() {
|
|||||||
data class NewConversationUiState(
|
data class NewConversationUiState(
|
||||||
val forceSplitPaneOnCompactLandscape: Boolean = SignalStore.internal.forceSplitPaneOnCompactLandscape,
|
val forceSplitPaneOnCompactLandscape: Boolean = SignalStore.internal.forceSplitPaneOnCompactLandscape,
|
||||||
val isRefreshingRecipient: Boolean = false,
|
val isRefreshingRecipient: Boolean = false,
|
||||||
|
val isRefreshingContacts: Boolean = false,
|
||||||
|
val shouldResetContactsList: Boolean = false,
|
||||||
val pendingDestination: RecipientId? = null,
|
val pendingDestination: RecipientId? = null,
|
||||||
val userMessage: UserMessage? = null
|
val userMessage: UserMessage? = null
|
||||||
) {
|
) {
|
||||||
sealed interface UserMessage {
|
sealed interface UserMessage {
|
||||||
data class RecipientNotSignalUser(val phone: PhoneNumber?) : UserMessage
|
sealed interface Info : UserMessage {
|
||||||
data object NetworkError : UserMessage
|
data class RecipientRemoved(val recipient: Recipient) : Info
|
||||||
|
data class RecipientBlocked(val recipient: Recipient) : Info
|
||||||
|
data class RecipientNotSignalUser(val phone: PhoneNumber?) : Info
|
||||||
|
data object UserAlreadyInAnotherCall : Info
|
||||||
|
data object NetworkError : Info
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface Prompt : UserMessage {
|
||||||
|
data class ConfirmRemoveRecipient(val recipient: Recipient) : Prompt
|
||||||
|
data class ConfirmBlockRecipient(val recipient: Recipient) : Prompt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.conversation
|
package org.thoughtcrime.securesms.conversation
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -16,21 +18,35 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.fragment.compose.rememberFragmentState
|
import androidx.fragment.compose.rememberFragmentState
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.signal.core.ui.compose.DayNightPreviews
|
import org.signal.core.ui.compose.DayNightPreviews
|
||||||
import org.signal.core.ui.compose.Fragments
|
import org.signal.core.ui.compose.Fragments
|
||||||
|
import org.signal.core.util.DimensionUnit
|
||||||
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
import org.thoughtcrime.securesms.ContactSelectionListFragment
|
||||||
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.components.ContactFilterView
|
import org.thoughtcrime.securesms.components.ContactFilterView
|
||||||
|
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||||
|
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||||
import org.thoughtcrime.securesms.contacts.paged.ChatType
|
import org.thoughtcrime.securesms.contacts.paged.ChatType
|
||||||
|
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||||
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments
|
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments
|
||||||
import org.thoughtcrime.securesms.recipients.PhoneNumber
|
import org.thoughtcrime.securesms.recipients.PhoneNumber
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
|
|
||||||
@@ -42,6 +58,9 @@ fun RecipientPicker(
|
|||||||
enableCreateNewGroup: Boolean,
|
enableCreateNewGroup: Boolean,
|
||||||
enableFindByUsername: Boolean,
|
enableFindByUsername: Boolean,
|
||||||
enableFindByPhoneNumber: Boolean,
|
enableFindByPhoneNumber: Boolean,
|
||||||
|
isRefreshing: Boolean,
|
||||||
|
focusAndShowKeyboard: Boolean = LocalConfiguration.current.screenHeightDp.dp > 600.dp,
|
||||||
|
shouldResetContactsList: Boolean,
|
||||||
callbacks: RecipientPickerCallbacks,
|
callbacks: RecipientPickerCallbacks,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -54,6 +73,7 @@ fun RecipientPicker(
|
|||||||
onFilterChanged = { filter ->
|
onFilterChanged = { filter ->
|
||||||
searchQuery = filter
|
searchQuery = filter
|
||||||
},
|
},
|
||||||
|
focusAndShowKeyboard = focusAndShowKeyboard,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
@@ -64,6 +84,8 @@ fun RecipientPicker(
|
|||||||
enableCreateNewGroup = enableCreateNewGroup,
|
enableCreateNewGroup = enableCreateNewGroup,
|
||||||
enableFindByUsername = enableFindByUsername,
|
enableFindByUsername = enableFindByUsername,
|
||||||
enableFindByPhoneNumber = enableFindByPhoneNumber,
|
enableFindByPhoneNumber = enableFindByPhoneNumber,
|
||||||
|
isRefreshing = isRefreshing,
|
||||||
|
shouldResetContactsList = shouldResetContactsList,
|
||||||
callbacks = callbacks,
|
callbacks = callbacks,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -81,6 +103,7 @@ fun RecipientPicker(
|
|||||||
private fun RecipientSearchField(
|
private fun RecipientSearchField(
|
||||||
onFilterChanged: (String) -> Unit,
|
onFilterChanged: (String) -> Unit,
|
||||||
@StringRes hintText: Int? = null,
|
@StringRes hintText: Int? = null,
|
||||||
|
focusAndShowKeyboard: Boolean = false,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -90,6 +113,17 @@ private fun RecipientSearchField(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(jeff) This causes the keyboard to re-open on rotation, which doesn't match the existing behavior of ContactFilterView. To fix this,
|
||||||
|
// RecipientSearchField needs to be converted to compose so we can use FocusRequestor.
|
||||||
|
LaunchedEffect(focusAndShowKeyboard) {
|
||||||
|
if (focusAndShowKeyboard) {
|
||||||
|
wrappedView.focusAndShowKeyboard()
|
||||||
|
} else {
|
||||||
|
wrappedView.clearFocus()
|
||||||
|
ViewUtil.hideKeyboard(wrappedView.context, wrappedView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DisposableEffect(onFilterChanged) {
|
DisposableEffect(onFilterChanged) {
|
||||||
wrappedView.setOnFilterChangedListener { filter -> onFilterChanged(filter) }
|
wrappedView.setOnFilterChangedListener { filter -> onFilterChanged(filter) }
|
||||||
onDispose {
|
onDispose {
|
||||||
@@ -109,6 +143,8 @@ private fun RecipientSearchResultsList(
|
|||||||
enableCreateNewGroup: Boolean,
|
enableCreateNewGroup: Boolean,
|
||||||
enableFindByUsername: Boolean,
|
enableFindByUsername: Boolean,
|
||||||
enableFindByPhoneNumber: Boolean,
|
enableFindByPhoneNumber: Boolean,
|
||||||
|
isRefreshing: Boolean,
|
||||||
|
shouldResetContactsList: Boolean,
|
||||||
callbacks: RecipientPickerCallbacks,
|
callbacks: RecipientPickerCallbacks,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
@@ -120,6 +156,7 @@ private fun RecipientSearchResultsList(
|
|||||||
|
|
||||||
val fragmentState = rememberFragmentState()
|
val fragmentState = rememberFragmentState()
|
||||||
var currentFragment by remember { mutableStateOf<ContactSelectionListFragment?>(null) }
|
var currentFragment by remember { mutableStateOf<ContactSelectionListFragment?>(null) }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
Fragments.Fragment<ContactSelectionListFragment>(
|
Fragments.Fragment<ContactSelectionListFragment>(
|
||||||
arguments = fragmentArgs,
|
arguments = fragmentArgs,
|
||||||
@@ -131,7 +168,8 @@ private fun RecipientSearchResultsList(
|
|||||||
callbacks = callbacks,
|
callbacks = callbacks,
|
||||||
enableCreateNewGroup = enableCreateNewGroup,
|
enableCreateNewGroup = enableCreateNewGroup,
|
||||||
enableFindByUsername = enableFindByUsername,
|
enableFindByUsername = enableFindByUsername,
|
||||||
enableFindByPhoneNumber = enableFindByPhoneNumber
|
enableFindByPhoneNumber = enableFindByPhoneNumber,
|
||||||
|
coroutineScope = coroutineScope
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -148,13 +186,30 @@ private fun RecipientSearchResultsList(
|
|||||||
previousQueryText = searchQuery
|
previousQueryText = searchQuery
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var wasRefreshing by rememberSaveable { mutableStateOf(isRefreshing) }
|
||||||
|
LaunchedEffect(isRefreshing) {
|
||||||
|
currentFragment?.isRefreshing = isRefreshing
|
||||||
|
if (wasRefreshing && !isRefreshing) {
|
||||||
|
currentFragment?.onDataRefreshed()
|
||||||
|
}
|
||||||
|
wasRefreshing = isRefreshing
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(shouldResetContactsList) {
|
||||||
|
if (shouldResetContactsList) {
|
||||||
|
currentFragment?.reset()
|
||||||
|
callbacks.onContactsListReset()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun ContactSelectionListFragment.setUpCallbacks(
|
private fun ContactSelectionListFragment.setUpCallbacks(
|
||||||
callbacks: RecipientPickerCallbacks,
|
callbacks: RecipientPickerCallbacks,
|
||||||
enableCreateNewGroup: Boolean,
|
enableCreateNewGroup: Boolean,
|
||||||
enableFindByUsername: Boolean,
|
enableFindByUsername: Boolean,
|
||||||
enableFindByPhoneNumber: Boolean
|
enableFindByPhoneNumber: Boolean,
|
||||||
|
coroutineScope: CoroutineScope
|
||||||
) {
|
) {
|
||||||
val fragment: ContactSelectionListFragment = this
|
val fragment: ContactSelectionListFragment = this
|
||||||
|
|
||||||
@@ -194,6 +249,83 @@ private fun ContactSelectionListFragment.setUpCallbacks(
|
|||||||
override fun onContactDeselected(recipientId: Optional<RecipientId?>, number: String?, chatType: Optional<ChatType?>) = Unit
|
override fun onContactDeselected(recipientId: Optional<RecipientId?>, number: String?, chatType: Optional<ChatType?>) = Unit
|
||||||
override fun onSelectionChanged() = Unit
|
override fun onSelectionChanged() = Unit
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fragment.setOnItemLongClickListener { anchorView, contactSearchKey, recyclerView ->
|
||||||
|
coroutineScope.launch { showItemContextMenu(anchorView, contactSearchKey, recyclerView, callbacks) }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment.setOnRefreshListener(callbacks::onRefresh)
|
||||||
|
fragment.setScrollCallback {
|
||||||
|
fragment.view?.let { view -> ViewUtil.hideKeyboard(view.context, view) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun showItemContextMenu(anchorView: View, contactSearchKey: ContactSearchKey, recyclerView: RecyclerView, callbacks: RecipientPickerCallbacks) {
|
||||||
|
val context = anchorView.context
|
||||||
|
val recipient = withContext(Dispatchers.IO) {
|
||||||
|
Recipient.resolved(contactSearchKey.requireRecipientSearchKey().recipientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val actions = buildList {
|
||||||
|
val messageItem = ActionItem(
|
||||||
|
iconRes = R.drawable.ic_chat_message_24,
|
||||||
|
title = context.getString(R.string.NewConversationActivity__message),
|
||||||
|
tintRes = R.color.signal_colorOnSurface,
|
||||||
|
action = { callbacks.onMessage(recipient.id) }
|
||||||
|
)
|
||||||
|
add(messageItem)
|
||||||
|
|
||||||
|
if (!recipient.isSelf && !recipient.isGroup && recipient.isRegistered) {
|
||||||
|
val voiceCallItem = ActionItem(
|
||||||
|
iconRes = R.drawable.ic_phone_right_24,
|
||||||
|
title = context.getString(R.string.NewConversationActivity__audio_call),
|
||||||
|
tintRes = R.color.signal_colorOnSurface,
|
||||||
|
action = { callbacks.onVoiceCall(recipient) }
|
||||||
|
)
|
||||||
|
add(voiceCallItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recipient.isSelf && !recipient.isMmsGroup && recipient.isRegistered) {
|
||||||
|
val videoCallItem = ActionItem(
|
||||||
|
iconRes = R.drawable.ic_video_call_24,
|
||||||
|
title = context.getString(R.string.NewConversationActivity__video_call),
|
||||||
|
tintRes = R.color.signal_colorOnSurface,
|
||||||
|
action = { callbacks.onVideoCall(recipient) }
|
||||||
|
)
|
||||||
|
add(videoCallItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recipient.isSelf && !recipient.isGroup) {
|
||||||
|
val removeItem = ActionItem(
|
||||||
|
iconRes = R.drawable.ic_minus_circle_20, // TODO [alex] -- correct asset
|
||||||
|
title = context.getString(R.string.NewConversationActivity__remove),
|
||||||
|
tintRes = R.color.signal_colorOnSurface,
|
||||||
|
action = { callbacks.onRemove(recipient) }
|
||||||
|
)
|
||||||
|
add(removeItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!recipient.isSelf) {
|
||||||
|
val blockItem = ActionItem(
|
||||||
|
iconRes = R.drawable.ic_block_tinted_24,
|
||||||
|
title = context.getString(R.string.NewConversationActivity__block),
|
||||||
|
tintRes = R.color.signal_colorError,
|
||||||
|
action = { callbacks.onBlock(recipient) }
|
||||||
|
)
|
||||||
|
add(blockItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalContextMenu.Builder(anchorView, anchorView.getRootView() as ViewGroup)
|
||||||
|
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.BELOW)
|
||||||
|
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START)
|
||||||
|
.offsetX(DimensionUnit.DP.toPixels(12f).toInt())
|
||||||
|
.offsetY(DimensionUnit.DP.toPixels(12f).toInt())
|
||||||
|
.onDismiss { recyclerView.suppressLayout(false) }
|
||||||
|
.show(actions)
|
||||||
|
|
||||||
|
recyclerView.suppressLayout(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@DayNightPreviews
|
@DayNightPreviews
|
||||||
@@ -203,6 +335,8 @@ private fun RecipientPickerPreview() {
|
|||||||
enableCreateNewGroup = true,
|
enableCreateNewGroup = true,
|
||||||
enableFindByUsername = true,
|
enableFindByUsername = true,
|
||||||
enableFindByPhoneNumber = true,
|
enableFindByPhoneNumber = true,
|
||||||
|
isRefreshing = false,
|
||||||
|
shouldResetContactsList = false,
|
||||||
callbacks = RecipientPickerCallbacks.Empty
|
callbacks = RecipientPickerCallbacks.Empty
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -220,7 +354,13 @@ interface RecipientPickerCallbacks {
|
|||||||
fun shouldAllowSelection(id: RecipientId): Boolean
|
fun shouldAllowSelection(id: RecipientId): Boolean
|
||||||
fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?)
|
fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?)
|
||||||
fun onMessage(id: RecipientId)
|
fun onMessage(id: RecipientId)
|
||||||
|
fun onVoiceCall(recipient: Recipient)
|
||||||
|
fun onVideoCall(recipient: Recipient)
|
||||||
|
fun onRemove(recipient: Recipient)
|
||||||
|
fun onBlock(recipient: Recipient)
|
||||||
fun onInviteToSignal()
|
fun onInviteToSignal()
|
||||||
|
fun onRefresh()
|
||||||
|
fun onContactsListReset()
|
||||||
|
|
||||||
object Empty : RecipientPickerCallbacks {
|
object Empty : RecipientPickerCallbacks {
|
||||||
override fun onCreateNewGroup() = Unit
|
override fun onCreateNewGroup() = Unit
|
||||||
@@ -229,6 +369,12 @@ interface RecipientPickerCallbacks {
|
|||||||
override fun shouldAllowSelection(id: RecipientId): Boolean = true
|
override fun shouldAllowSelection(id: RecipientId): Boolean = true
|
||||||
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = Unit
|
override fun onRecipientSelected(id: RecipientId?, phone: PhoneNumber?) = Unit
|
||||||
override fun onMessage(id: RecipientId) = Unit
|
override fun onMessage(id: RecipientId) = Unit
|
||||||
|
override fun onVoiceCall(recipient: Recipient) = Unit
|
||||||
|
override fun onVideoCall(recipient: Recipient) = Unit
|
||||||
|
override fun onRemove(recipient: Recipient) = Unit
|
||||||
|
override fun onBlock(recipient: Recipient) = Unit
|
||||||
override fun onInviteToSignal() = Unit
|
override fun onInviteToSignal() = Unit
|
||||||
|
override fun onRefresh() = Unit
|
||||||
|
override fun onContactsListReset() = Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5918,8 +5918,8 @@
|
|||||||
<string name="NewConversationActivity__view_contact">View contact</string>
|
<string name="NewConversationActivity__view_contact">View contact</string>
|
||||||
<!-- Error message shown when looking up a person by phone number and that phone number is not associated with a signal account -->
|
<!-- Error message shown when looking up a person by phone number and that phone number is not associated with a signal account -->
|
||||||
<string name="NewConversationActivity__s_is_not_a_signal_user">%1$s is not a Signal user</string>
|
<string name="NewConversationActivity__s_is_not_a_signal_user">%1$s is not a Signal user</string>
|
||||||
<!-- Error message shown when we could not get a user from the username link -->
|
<!-- Accessibility label for opening the screen specific dropdown menu. -->
|
||||||
<string name="NewConversationActivity__">%1$s is not a Signal user</string>
|
<string name="NewConversationActivity__accessibility_open_top_bar_menu">Open menu</string>
|
||||||
<!-- Error message shown in a dialog when trying to create a new group with non-signal users (e.g., unregistered or phone number only contacts) -->
|
<!-- Error message shown in a dialog when trying to create a new group with non-signal users (e.g., unregistered or phone number only contacts) -->
|
||||||
<plurals name="CreateGroupActivity_not_signal_users">
|
<plurals name="CreateGroupActivity_not_signal_users">
|
||||||
<item quantity="one">%1$s is not a Signal user</item>
|
<item quantity="one">%1$s is not a Signal user</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user