Restyling review banner and cards.

This commit is contained in:
Alex Hart
2023-12-20 13:55:40 -04:00
committed by Clark Chen
parent bb30535afb
commit ca9a629804
15 changed files with 548 additions and 227 deletions

View File

@@ -29,7 +29,7 @@ fun AvatarImage(
} else {
AndroidView(
factory = ::AvatarImageView,
modifier = modifier
modifier = modifier.background(color = Color.Transparent, shape = CircleShape)
) {
it.setAvatarUsingProfile(recipient)
}

View File

@@ -3,34 +3,26 @@ package org.thoughtcrime.securesms.profiles.spoofing;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto20dp;
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.databinding.ReviewBannerViewBinding;
import org.thoughtcrime.securesms.recipients.Recipient;
/**
* Banner displayed within a conversation when a review is suggested.
*/
public class ReviewBannerView extends LinearLayout {
public class ReviewBannerView extends FrameLayout {
private ImageView bannerIcon;
private TextView bannerMessage;
private View bannerClose;
private AvatarImageView topLeftAvatar;
private AvatarImageView bottomRightAvatar;
private View stroke;
private OnHideListener onHideListener;
private ReviewBannerViewBinding binding;
private OnHideListener onHideListener;
public ReviewBannerView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
@@ -44,19 +36,14 @@ public class ReviewBannerView extends LinearLayout {
protected void onFinishInflate() {
super.onFinishInflate();
bannerIcon = findViewById(R.id.banner_icon);
bannerMessage = findViewById(R.id.banner_message);
bannerClose = findViewById(R.id.banner_close);
topLeftAvatar = findViewById(R.id.banner_avatar_1);
bottomRightAvatar = findViewById(R.id.banner_avatar_2);
stroke = findViewById(R.id.banner_avatar_stroke);
binding = ReviewBannerViewBinding.bind(this);
FallbackPhotoProvider provider = new FallbackPhotoProvider();
topLeftAvatar.setFallbackPhotoProvider(provider);
bottomRightAvatar.setFallbackPhotoProvider(provider);
binding.bannerBottomRightAvatar.setFallbackPhotoProvider(provider);
binding.bannerTopLeftAvatar.setFallbackPhotoProvider(provider);
bannerClose.setOnClickListener(v -> {
binding.bannerClose.setOnClickListener(v -> {
if (onHideListener != null && onHideListener.onHide()) {
return;
}
@@ -70,26 +57,32 @@ public class ReviewBannerView extends LinearLayout {
}
public void setBannerMessage(@Nullable CharSequence charSequence) {
bannerMessage.setText(charSequence);
binding.bannerMessage.setText(charSequence);
}
public void setBannerIcon(@Nullable Drawable icon) {
bannerIcon.setImageDrawable(icon);
binding.bannerIcon.setImageDrawable(icon);
bannerIcon.setVisibility(VISIBLE);
topLeftAvatar.setVisibility(GONE);
bottomRightAvatar.setVisibility(GONE);
stroke.setVisibility(GONE);
binding.bannerIcon.setVisibility(VISIBLE);
binding.bannerTopLeftAvatar.setVisibility(GONE);
binding.bannerBottomRightAvatar.setVisibility(GONE);
binding.bannerAvatarStroke.setVisibility(GONE);
}
public void setBannerRecipient(@NonNull Recipient recipient) {
topLeftAvatar.setAvatar(recipient);
bottomRightAvatar.setAvatar(recipient);
binding.bannerTopLeftAvatar.setAvatar(recipient);
binding.bannerBottomRightAvatar.setAvatar(recipient);
bannerIcon.setVisibility(GONE);
topLeftAvatar.setVisibility(VISIBLE);
bottomRightAvatar.setVisibility(VISIBLE);
stroke.setVisibility(VISIBLE);
binding.bannerIcon.setVisibility(GONE);
binding.bannerTopLeftAvatar.setVisibility(VISIBLE);
binding.bannerBottomRightAvatar.setVisibility(VISIBLE);
binding.bannerAvatarStroke.setVisibility(VISIBLE);
}
@Override
public void setOnClickListener(@Nullable OnClickListener l) {
super.setOnClickListener(l);
binding.bannerTapToReview.setOnClickListener(l);
}
private static final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {

View File

@@ -44,6 +44,7 @@ class ReviewCardAdapter extends ListAdapter<ReviewCard, ReviewCardViewHolder> {
interface Callbacks {
void onCardClicked(@NonNull ReviewCard card);
void onActionClicked(@NonNull ReviewCard card, @NonNull ReviewCard.Action action);
void onSignalConnectionClicked();
}
private final class CallbacksAdapter implements ReviewCardViewHolder.Callbacks {
@@ -70,5 +71,10 @@ class ReviewCardAdapter extends ListAdapter<ReviewCard, ReviewCardViewHolder> {
ReviewCard card = getItem(position);
callback.onActionClicked(card, Objects.requireNonNull(card.getSecondaryAction()));
}
@Override
public void onSignalConnectionClicked() {
callback.onSignalConnectionClicked();
}
}
}

View File

@@ -22,6 +22,8 @@ import org.thoughtcrime.securesms.groups.BadGroupIdException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.stories.settings.my.SignalConnectionsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
public class ReviewCardDialogFragment extends FullScreenDialogFragment {
@@ -202,5 +204,10 @@ public class ReviewCardDialogFragment extends FullScreenDialogFragment {
viewModel.act(card, action);
}
}
@Override
public void onSignalConnectionClicked() {
new SignalConnectionsBottomSheetDialogFragment().show(getParentFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
}
}

View File

@@ -1,31 +1,37 @@
package org.thoughtcrime.securesms.profiles.spoofing;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.SpannableStringBuilder;
import android.util.Pair;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.PluralsRes;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.databinding.ReviewCardBinding;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.whispersystems.signalservice.api.util.Preconditions;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class ReviewCardViewHolder extends RecyclerView.ViewHolder {
private final int noGroupsInCommonResId;
private final int groupsInCommonResId;
private final TextView title;
private final AvatarImageView avatar;
private final TextView name;
private final TextView subtextLine1;
private final TextView subtextLine2;
private final Button primaryAction;
private final Button secondaryAction;
private final int noGroupsInCommonResId;
private final int groupsInCommonResId;
private final ReviewCardBinding binding;
private final List<Pair<TextView, ImageView>> subtextGroups;
private final Runnable onSignalConnectionClicked;
public ReviewCardViewHolder(@NonNull View itemView,
@StringRes int noGroupsInCommonResId,
@@ -36,69 +42,188 @@ class ReviewCardViewHolder extends RecyclerView.ViewHolder {
this.noGroupsInCommonResId = noGroupsInCommonResId;
this.groupsInCommonResId = groupsInCommonResId;
this.title = itemView.findViewById(R.id.card_title);
this.avatar = itemView.findViewById(R.id.card_avatar);
this.name = itemView.findViewById(R.id.card_name);
this.subtextLine1 = itemView.findViewById(R.id.card_subtext_line1);
this.subtextLine2 = itemView.findViewById(R.id.card_subtext_line2);
this.primaryAction = itemView.findViewById(R.id.card_primary_action_button);
this.secondaryAction = itemView.findViewById(R.id.card_secondary_action_button);
this.binding = ReviewCardBinding.bind(itemView);
this.subtextGroups = Arrays.asList(
Pair.create(binding.cardSubtextLine1, binding.cardSubtextIcon1),
Pair.create(binding.cardSubtextLine2, binding.cardSubtextIcon2),
Pair.create(binding.cardSubtextLine3, binding.cardSubtextIcon3),
Pair.create(binding.cardSubtextLine4, binding.cardSubtextIcon4)
);
itemView.findViewById(R.id.card_tap_target).setOnClickListener(unused -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
callbacks.onCardClicked(getAdapterPosition());
if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
callbacks.onCardClicked(getBindingAdapterPosition());
}
});
primaryAction.setOnClickListener(unused -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
callbacks.onPrimaryActionItemClicked(getAdapterPosition());
binding.cardPrimaryActionButton.setOnClickListener(unused -> {
if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
callbacks.onPrimaryActionItemClicked(getBindingAdapterPosition());
}
});
secondaryAction.setOnClickListener(unused -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
callbacks.onSecondaryActionItemClicked(getAdapterPosition());
binding.cardSecondaryActionButton.setOnClickListener(unused -> {
if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
callbacks.onSecondaryActionItemClicked(getBindingAdapterPosition());
}
});
onSignalConnectionClicked = callbacks::onSignalConnectionClicked;
}
void bind(@NonNull ReviewCard reviewCard) {
Context context = itemView.getContext();
avatar.setAvatar(reviewCard.getReviewRecipient());
name.setText(reviewCard.getReviewRecipient().getDisplayName(context));
title.setText(getTitleResId(reviewCard.getCardType()));
binding.cardAvatar.setAvatarUsingProfile(reviewCard.getReviewRecipient());
switch (reviewCard.getCardType()) {
case MEMBER:
case REQUEST:
setNonContactSublines(context, reviewCard);
break;
case YOUR_CONTACT:
subtextLine1.setText(reviewCard.getReviewRecipient().getE164().orElse(null));
subtextLine2.setText(getGroupsInCommon(reviewCard.getInCommonGroupsCount()));
break;
default:
throw new AssertionError();
String name = reviewCard.getReviewRecipient().isSelf()
? context.getString(R.string.AboutSheet__you)
: reviewCard.getReviewRecipient().getDisplayName(context);
binding.cardName.setText(name);
int titleTextResId = getTitleResId(reviewCard.getCardType());
if (titleTextResId > 0) {
binding.cardTitle.setText(getTitleResId(reviewCard.getCardType()));
} else {
binding.cardTitle.setVisibility(View.GONE);
}
List<ReviewTextRow> rows = switch (reviewCard.getCardType()) {
case MEMBER, REQUEST -> getNonContactSublines(reviewCard);
case YOUR_CONTACT -> getContactSublines(reviewCard);
};
presentReviewTextRows(rows, context, reviewCard);
setActions(reviewCard);
}
private void setNonContactSublines(@NonNull Context context, @NonNull ReviewCard reviewCard) {
subtextLine1.setText(getGroupsInCommon(reviewCard.getInCommonGroupsCount()));
private List<ReviewTextRow> getNonContactSublines(@NonNull ReviewCard reviewCard) {
List<ReviewTextRow> reviewTextRows = new ArrayList<>(subtextGroups.size());
if (reviewCard.getReviewRecipient().isProfileSharing() && !reviewCard.getReviewRecipient().isSelf()) {
reviewTextRows.add(ReviewTextRow.SIGNAL_CONNECTION);
}
if (reviewCard.getReviewRecipient().isSystemContact()) {
reviewTextRows.add(ReviewTextRow.SYSTEM_CONTACTS);
}
if (reviewCard.getNameChange() != null) {
subtextLine2.setText(SpanUtil.italic(context.getString(R.string.ReviewCard__recently_changed,
reviewCard.getNameChange().previous,
reviewCard.getNameChange().newValue)));
reviewTextRows.add(ReviewTextRow.RECENTLY_CHANGED);
}
reviewTextRows.add(ReviewTextRow.GROUPS_IN_COMMON);
return reviewTextRows;
}
private List<ReviewTextRow> getContactSublines(@NonNull ReviewCard reviewCard) {
List<ReviewTextRow> reviewTextRows = new ArrayList<>(subtextGroups.size());
if (reviewCard.getReviewRecipient().isProfileSharing() && !reviewCard.getReviewRecipient().isSelf()) {
reviewTextRows.add(ReviewTextRow.SIGNAL_CONNECTION);
}
if (reviewCard.getReviewRecipient().isSystemContact()) {
reviewTextRows.add(ReviewTextRow.SYSTEM_CONTACTS);
}
if (reviewCard.getReviewRecipient().hasE164() && reviewCard.getReviewRecipient().shouldShowE164()) {
reviewTextRows.add(ReviewTextRow.PHONE_NUMBER);
}
reviewTextRows.add(ReviewTextRow.GROUPS_IN_COMMON);
return reviewTextRows;
}
private void presentReviewTextRows(@NonNull List<ReviewTextRow> reviewTextRows, @NonNull Context context, @NonNull ReviewCard reviewCard) {
for (Pair<TextView, ImageView> group : subtextGroups) {
setVisibility(View.GONE, group.first, group.second);
}
for (int i = 0; i < Math.min(reviewTextRows.size(), subtextGroups.size()); i++) {
ReviewTextRow row = reviewTextRows.get(i);
Pair<TextView, ImageView> group = subtextGroups.get(i);
setVisibility(View.VISIBLE, group.first, group.second);
switch (row) {
case SIGNAL_CONNECTION -> presentSignalConnection(group.first, group.second, context, reviewCard);
case PHONE_NUMBER -> presentPhoneNumber(group.first, group.second, reviewCard);
case RECENTLY_CHANGED -> presentRecentlyChanged(group.first, group.second, context, reviewCard);
case GROUPS_IN_COMMON -> presentGroupsInCommon(group.first, group.second, reviewCard);
case SYSTEM_CONTACTS -> presentSystemContacts(group.first, group.second, context, reviewCard);
}
}
}
private void presentSignalConnection(@NonNull TextView line, @NonNull ImageView icon, @NonNull Context context, @NonNull ReviewCard reviewCard) {
Preconditions.checkArgument(reviewCard.getReviewRecipient().isProfileSharing());
Drawable chevron = ContextCompat.getDrawable(context, R.drawable.symbol_chevron_right_24);
Preconditions.checkNotNull(chevron);
chevron.setTint(ContextCompat.getColor(context, R.color.core_grey_45));
SpannableStringBuilder builder = new SpannableStringBuilder(context.getString(R.string.AboutSheet__signal_connection));
SpanUtil.appendCenteredImageSpan(builder, chevron, 20, 20);
icon.setImageResource(R.drawable.symbol_connections_compact_16);
line.setText(builder);
line.setOnClickListener(v -> onSignalConnectionClicked.run());
}
private void presentPhoneNumber(@NonNull TextView line, @NonNull ImageView icon, @NonNull ReviewCard reviewCard) {
icon.setImageResource(R.drawable.symbol_phone_compact_16);
line.setText(reviewCard.getReviewRecipient().requireE164());
line.setOnClickListener(null);
line.setClickable(false);
}
private void presentRecentlyChanged(@NonNull TextView line, @NonNull ImageView icon, @NonNull Context context, @NonNull ReviewCard reviewCard) {
Preconditions.checkNotNull(reviewCard.getNameChange());
icon.setImageResource(R.drawable.symbol_person_compact_16);
line.setText(context.getString(R.string.ReviewCard__s_recently_changed,
reviewCard.getReviewRecipient().getShortDisplayName(context),
reviewCard.getNameChange().previous,
reviewCard.getNameChange().newValue));
line.setOnClickListener(null);
line.setClickable(false);
}
private void presentGroupsInCommon(@NonNull TextView line, @NonNull ImageView icon, @NonNull ReviewCard reviewCard) {
icon.setImageResource(R.drawable.symbol_group_compact_16);
line.setText(getGroupsInCommon(reviewCard.getInCommonGroupsCount()));
line.setOnClickListener(null);
line.setClickable(false);
}
private void presentSystemContacts(@NonNull TextView line, @NonNull ImageView icon, @NonNull Context context, @NonNull ReviewCard reviewCard) {
icon.setImageResource(R.drawable.symbol_person_circle_compat_16);
line.setText(context.getString(R.string.ReviewCard__s_is_in_your_system_contacts, reviewCard.getReviewRecipient().getShortDisplayName(context)));
line.setOnClickListener(null);
line.setClickable(false);
}
private void setVisibility(int visibility, View... views) {
for (View view : views) {
view.setVisibility(visibility);
}
}
private void setActions(@NonNull ReviewCard reviewCard) {
setAction(reviewCard.getPrimaryAction(), primaryAction);
setAction(reviewCard.getSecondaryAction(), secondaryAction);
if (reviewCard.getReviewRecipient().isSelf()) {
setAction(null, binding.cardPrimaryActionButton);
setAction(null, binding.cardSecondaryActionButton);
} else {
setAction(reviewCard.getPrimaryAction(), binding.cardPrimaryActionButton);
setAction(reviewCard.getSecondaryAction(), binding.cardSecondaryActionButton);
}
}
private String getGroupsInCommon(int groupsInCommon) {
@@ -120,35 +245,36 @@ class ReviewCardViewHolder extends RecyclerView.ViewHolder {
interface Callbacks {
void onCardClicked(int position);
void onPrimaryActionItemClicked(int position);
void onSecondaryActionItemClicked(int position);
void onSignalConnectionClicked();
}
private static @StringRes int getTitleResId(@NonNull ReviewCard.CardType cardType) {
switch (cardType) {
case MEMBER:
return R.string.ReviewCard__member;
case REQUEST:
return R.string.ReviewCard__request;
case YOUR_CONTACT:
return R.string.ReviewCard__your_contact;
default:
throw new IllegalArgumentException("Unsupported card type " + cardType);
}
return switch (cardType) {
case MEMBER -> -1;
case REQUEST -> R.string.ReviewCard__request;
case YOUR_CONTACT -> R.string.ReviewCard__your_contact;
};
}
private static @StringRes int getActionLabelResId(@NonNull ReviewCard.Action action) {
switch (action) {
case UPDATE_CONTACT:
return R.string.ReviewCard__update_contact;
case DELETE:
return R.string.ReviewCard__delete;
case BLOCK:
return R.string.ReviewCard__block;
case REMOVE_FROM_GROUP:
return R.string.ReviewCard__remove_from_group;
default:
throw new IllegalArgumentException("Unsupported action: " + action);
}
return switch (action) {
case UPDATE_CONTACT -> R.string.ReviewCard__update_contact;
case DELETE -> R.string.ReviewCard__delete;
case BLOCK -> R.string.ReviewCard__block;
case REMOVE_FROM_GROUP -> R.string.ReviewCard__remove_from_group;
};
}
private enum ReviewTextRow {
SIGNAL_CONNECTION,
PHONE_NUMBER,
RECENTLY_CHANGED,
GROUPS_IN_COMMON,
SYSTEM_CONTACTS
}
}

View File

@@ -127,17 +127,25 @@ private fun AboutSheetContent(
BottomSheets.Handle(modifier = Modifier.padding(top = 6.dp))
}
val avatarOnClick = remember(recipient.profileAvatarFileDetails.hasFile()) {
if (recipient.profileAvatarFileDetails.hasFile()) {
onAvatarClicked
} else {
{ }
}
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
AvatarImage(
recipient = recipient,
modifier = Modifier
.padding(top = 56.dp)
.size(240.dp)
.clickable(onClick = onAvatarClicked)
.clickable(onClick = avatarOnClick)
)
Text(
text = "About",
text = stringResource(id = if (recipient.isSelf) R.string.AboutSheet__you else R.string.AboutSheet__about),
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier
.fillMaxWidth()