Usernames 1.01 Fast-Follow Part 1.

This commit is contained in:
Alex Hart 2024-03-06 09:06:50 -04:00
parent 83c16a46de
commit 4b4b263423
8 changed files with 241 additions and 25 deletions

View file

@ -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);

View file

@ -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)
}

View file

@ -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
}
}
}
}
}

View file

@ -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);

View file

@ -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()
}
)
}

View file

@ -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)

View file

@ -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" />

View file

@ -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