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)