Usernames 1.01 Fast-Follow Part 1.
This commit is contained in:
parent
83c16a46de
commit
4b4b263423
8 changed files with 241 additions and 25 deletions
|
@ -890,11 +890,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
|||
return ContactSearchConfiguration.build(builder -> {
|
||||
builder.setQuery(contactSearchState.getQuery());
|
||||
|
||||
if (newConversationCallback != null) {
|
||||
if (newConversationCallback != null && !hasQuery) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
|
||||
}
|
||||
|
||||
if (findByCallback != null) {
|
||||
if (findByCallback != null && !hasQuery) {
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_BY_USERNAME.getCode());
|
||||
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_BY_PHONE_NUMBER.getCode());
|
||||
}
|
||||
|
@ -913,10 +913,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
|||
));
|
||||
}
|
||||
|
||||
boolean hideHeader = newCallCallback != null || (newConversationCallback != null && !hasQuery);
|
||||
builder.addSection(new ContactSearchConfiguration.Section.Individuals(
|
||||
includeSelf,
|
||||
transportType,
|
||||
newCallCallback == null && findByCallback == null,
|
||||
!hideHeader,
|
||||
null,
|
||||
!hideLetterHeaders()
|
||||
));
|
||||
|
@ -944,7 +945,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
|
|||
builder.username(newRowMode);
|
||||
}
|
||||
|
||||
if (newCallCallback != null || newConversationCallback != null) {
|
||||
if ((newCallCallback != null || newConversationCallback != null) && !hasQuery) {
|
||||
addMoreSection(builder);
|
||||
builder.withEmptyState(emptyBuilder -> {
|
||||
emptyBuilder.addSection(ContactSearchConfiguration.Section.Empty.INSTANCE);
|
||||
|
|
|
@ -398,15 +398,22 @@ open class ContactSearchAdapter(
|
|||
override fun bind(model: UnknownRecipientModel) {
|
||||
checkbox.visible = displayCheckBox
|
||||
checkbox.isSelected = false
|
||||
name.setText(
|
||||
when (model.data.mode) {
|
||||
ContactSearchConfiguration.NewRowMode.NEW_CALL -> R.string.contact_selection_list__new_call
|
||||
ContactSearchConfiguration.NewRowMode.NEW_CONVERSATION -> R.string.contact_selection_list__unknown_contact
|
||||
ContactSearchConfiguration.NewRowMode.BLOCK -> R.string.contact_selection_list__unknown_contact_block
|
||||
ContactSearchConfiguration.NewRowMode.ADD_TO_GROUP -> R.string.contact_selection_list__unknown_contact_add_to_group
|
||||
}
|
||||
)
|
||||
number.text = model.data.query
|
||||
val nameText = when (model.data.mode) {
|
||||
ContactSearchConfiguration.NewRowMode.NEW_CALL -> R.string.contact_selection_list__new_call
|
||||
ContactSearchConfiguration.NewRowMode.NEW_CONVERSATION -> -1
|
||||
ContactSearchConfiguration.NewRowMode.BLOCK -> R.string.contact_selection_list__unknown_contact_block
|
||||
ContactSearchConfiguration.NewRowMode.ADD_TO_GROUP -> R.string.contact_selection_list__unknown_contact_add_to_group
|
||||
}
|
||||
|
||||
if (nameText > 0) {
|
||||
name.setText(nameText)
|
||||
number.text = model.data.query
|
||||
number.visible = true
|
||||
} else {
|
||||
name.text = model.data.query
|
||||
number.visible = false
|
||||
}
|
||||
|
||||
itemView.setOnClickListener {
|
||||
onClick.onClicked(itemView, model.data, false)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.permissions.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import org.signal.core.ui.Dialogs
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
/**
|
||||
* Dialogs and state management for permissions requests in compose screens.
|
||||
*/
|
||||
object Permissions {
|
||||
|
||||
interface Controller {
|
||||
fun request()
|
||||
}
|
||||
|
||||
private enum class RequestState {
|
||||
NONE,
|
||||
RATIONALE,
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun cameraPermissionHandler(
|
||||
rationale: String,
|
||||
onPermissionGranted: () -> Unit
|
||||
): Controller {
|
||||
return permissionHandler(
|
||||
permission = android.Manifest.permission.CAMERA,
|
||||
icon = painterResource(id = R.drawable.symbol_camera_24),
|
||||
rationale = rationale,
|
||||
onPermissionGranted = onPermissionGranted
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic permissions rationale dialog and state management for single permissions.
|
||||
*/
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun permissionHandler(
|
||||
permission: String,
|
||||
icon: Painter,
|
||||
rationale: String,
|
||||
onPermissionGranted: () -> Unit
|
||||
): Controller {
|
||||
var requestState by remember {
|
||||
mutableStateOf(RequestState.NONE)
|
||||
}
|
||||
|
||||
val permissionState = rememberPermissionState(permission = permission) {
|
||||
if (it && requestState == RequestState.SYSTEM) {
|
||||
onPermissionGranted()
|
||||
}
|
||||
}
|
||||
|
||||
if (requestState == RequestState.RATIONALE) {
|
||||
Dialogs.PermissionRationaleDialog(
|
||||
icon = icon,
|
||||
rationale = rationale,
|
||||
confirm = stringResource(id = R.string.Permissions_continue),
|
||||
dismiss = stringResource(id = R.string.Permissions_not_now),
|
||||
onConfirm = {
|
||||
requestState = RequestState.SYSTEM
|
||||
permissionState.launchPermissionRequest()
|
||||
},
|
||||
onDismiss = {
|
||||
requestState = RequestState.NONE
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return object : Controller {
|
||||
override fun request() {
|
||||
if (permissionState.status.isGranted) {
|
||||
requestState = RequestState.NONE
|
||||
onPermissionGranted()
|
||||
} else {
|
||||
requestState = RequestState.RATIONALE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,8 @@ import android.app.Activity;
|
|||
import android.content.ActivityNotFoundException;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
@ -185,25 +187,20 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
: recipient.getDisplayName(requireContext());
|
||||
fullName.setVisibility(TextUtils.isEmpty(name) ? View.GONE : View.VISIBLE);
|
||||
SpannableStringBuilder nameBuilder = new SpannableStringBuilder(name);
|
||||
boolean appendedToName = false;
|
||||
if (recipient.showVerified()) {
|
||||
appendedToName = true;
|
||||
SpanUtil.appendCenteredImageSpan(nameBuilder, ContextUtil.requireDrawable(requireContext(), R.drawable.ic_official_28), 28, 28);
|
||||
SpanUtil.appendCenteredImageSpanWithoutSpace(nameBuilder, new ColorDrawable(Color.TRANSPARENT), 8, 8);
|
||||
SpanUtil.appendCenteredImageSpanWithoutSpace(nameBuilder, ContextUtil.requireDrawable(requireContext(), R.drawable.ic_official_28), 28, 28);
|
||||
} else if (recipient.isSystemContact()) {
|
||||
appendedToName = true;
|
||||
Drawable drawable = ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_person_circle_24);
|
||||
drawable.setTint(ContextCompat.getColor(requireContext(), R.color.signal_colorOnSurface));
|
||||
SpanUtil.appendCenteredImageSpan(nameBuilder, drawable, 24, 24);
|
||||
SpanUtil.appendCenteredImageSpanWithoutSpace(nameBuilder, new ColorDrawable(Color.TRANSPARENT), 8, 8);
|
||||
SpanUtil.appendCenteredImageSpanWithoutSpace(nameBuilder, drawable, 24, 24);
|
||||
}
|
||||
|
||||
if (!recipient.isSelf() && recipient.isIndividual()) {
|
||||
Drawable drawable = ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_chevron_right_24);
|
||||
drawable.setBounds(0, 0, (int) DimensionUnit.DP.toPixels(24), (int) DimensionUnit.DP.toPixels(24));
|
||||
drawable.setTint(ContextCompat.getColor(requireContext(), R.color.signal_colorOutline));
|
||||
|
||||
if (!appendedToName) {
|
||||
nameBuilder.append(" ");
|
||||
}
|
||||
nameBuilder.append(SpanUtil.buildCenteredImageSpan(drawable));
|
||||
|
||||
fullName.setText(nameBuilder);
|
||||
|
|
|
@ -66,6 +66,7 @@ import androidx.navigation.compose.composable
|
|||
import androidx.navigation.compose.dialog
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.Animations.navHostSlideInTransition
|
||||
import org.signal.core.ui.Animations.navHostSlideOutTransition
|
||||
|
@ -81,6 +82,7 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity
|
|||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameQrScannerActivity
|
||||
import org.thoughtcrime.securesms.invites.InviteActions
|
||||
import org.thoughtcrime.securesms.permissions.compose.Permissions
|
||||
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberVisualTransformation
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
@ -103,6 +105,7 @@ class FindByActivity : PassphraseRequiredActivity() {
|
|||
FindByViewModel(FindByMode.valueOf(intent.getStringExtra(MODE)!!))
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||
val qrScanLauncher: ActivityResultLauncher<Unit> = registerForActivityResult(UsernameQrScannerActivity.Contract()) { recipientId ->
|
||||
if (recipientId != null) {
|
||||
|
@ -136,6 +139,14 @@ class FindByActivity : PassphraseRequiredActivity() {
|
|||
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val cameraPermissionController = Permissions.cameraPermissionHandler(
|
||||
rationale = stringResource(id = R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs_access_to_the_camera),
|
||||
onPermissionGranted = {
|
||||
qrScanLauncher.launch(Unit)
|
||||
}
|
||||
)
|
||||
|
||||
Content(
|
||||
paddingValues = it,
|
||||
state = state,
|
||||
|
@ -157,7 +168,7 @@ class FindByActivity : PassphraseRequiredActivity() {
|
|||
navController.navigate("select-country-prefix")
|
||||
},
|
||||
onQrCodeScanClicked = {
|
||||
qrScanLauncher.launch(Unit)
|
||||
cameraPermissionController.request()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -147,6 +147,11 @@ public final class SpanUtil {
|
|||
builder.append(" ").append(SpanUtil.buildCenteredImageSpan(drawable));
|
||||
}
|
||||
|
||||
public static void appendCenteredImageSpanWithoutSpace(@NonNull SpannableStringBuilder builder, @NonNull Drawable drawable, int width, int height) {
|
||||
drawable.setBounds(0, 0, ViewUtil.dpToPx(width), ViewUtil.dpToPx(height));
|
||||
builder.append(SpanUtil.buildCenteredImageSpan(drawable));
|
||||
}
|
||||
|
||||
public static CharSequence learnMore(@NonNull Context context,
|
||||
@ColorInt int color,
|
||||
@NonNull View.OnClickListener onLearnMoreClicked)
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Signal.Circle"
|
||||
app:srcCompat="@drawable/ic_search_24"
|
||||
app:srcCompat="@drawable/symbol_search_24"
|
||||
app:tint="@color/signal_colorOnSecondaryContainer"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
|
|
|
@ -1,17 +1,34 @@
|
|||
package org.signal.core.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import org.signal.core.ui.Dialogs.PermissionRationaleDialog
|
||||
import org.signal.core.ui.Dialogs.SimpleAlertDialog
|
||||
import org.signal.core.ui.Dialogs.SimpleMessageDialog
|
||||
|
||||
|
@ -31,7 +48,11 @@ object Dialogs {
|
|||
) {
|
||||
androidx.compose.material3.AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = if (title == null) null else { { Text(text = title) } },
|
||||
title = if (title == null) {
|
||||
null
|
||||
} else {
|
||||
{ Text(text = title) }
|
||||
},
|
||||
text = { Text(text = message) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
|
@ -105,6 +126,82 @@ object Dialogs {
|
|||
.size(100.dp)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun PermissionRationaleDialog(
|
||||
icon: Painter,
|
||||
rationale: String,
|
||||
confirm: String,
|
||||
dismiss: String,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
shape = AlertDialogDefaults.shape
|
||||
)
|
||||
.clip(AlertDialogDefaults.shape)
|
||||
) {
|
||||
Column {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = MaterialTheme.colorScheme.primary)
|
||||
.padding(40.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = icon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = rationale,
|
||||
modifier = Modifier
|
||||
.padding(top = 20.dp)
|
||||
.padding(horizontal = 20.dp)
|
||||
)
|
||||
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp)
|
||||
) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(text = dismiss)
|
||||
}
|
||||
|
||||
TextButton(onClick = onConfirm) {
|
||||
Text(text = confirm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PermissionRationaleDialogPreview() {
|
||||
Previews.Preview {
|
||||
PermissionRationaleDialog(
|
||||
icon = painterResource(id = android.R.drawable.ic_menu_camera),
|
||||
rationale = "This is rationale text about why we need permission.",
|
||||
confirm = "Continue",
|
||||
dismiss = "Not now",
|
||||
onConfirm = {},
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
Loading…
Add table
Reference in a new issue