Block avatar downloads in message request states.

This commit is contained in:
Michelle Tang
2025-03-06 12:36:58 -05:00
parent 5592d13258
commit 451d12ed53
28 changed files with 991 additions and 492 deletions

View File

@@ -257,6 +257,14 @@ class Recipient(
null
}
/**
* Whether or not a recipient (either individual or group) has a corresponding avatar
*/
val hasAvatar: Boolean
get() {
return (isIndividual && profileAvatar != null) || (isGroup && groupAvatarId.orElse(0L) != 0L)
}
/** The URI of the ringtone that should be used when receiving a message from this recipient, if set. */
val messageRingtone: Uri? by lazy {
if (messageRingtoneUri != null && messageRingtoneUri.scheme != null && messageRingtoneUri.scheme!!.startsWith("file")) {
@@ -378,6 +386,12 @@ class Recipient(
return !showOverride && !isSelf && !isProfileSharing && !isSystemContact && !hasGroupsInCommon && isRegistered
}
/** Whether or not the recipient's avatar should be shown in the chat list by default. Even if false, user can still manually choose to show the avatar */
val shouldShowAvatarByDefault: Boolean
get() {
return (isSelf || isProfileSharing || isSystemContact || hasGroupsInCommon) && isRegistered
}
/** The chat color to use when the "automatic" chat color setting is active, which derives a color from the wallpaper. */
private val autoChatColor: ChatColors
get() = wallpaper?.autoChatColors ?: ChatColorsPalette.Bubbles.default.withId(Auto)
@@ -801,7 +815,8 @@ class Recipient(
callLinkRoomId == other.callLinkRoomId &&
phoneNumberSharing == other.phoneNumberSharing &&
nickname == other.nickname &&
note == other.note
note == other.note &&
shouldBlurAvatar == other.shouldBlurAvatar
}
override fun equals(other: Any?): Boolean {

View File

@@ -1,418 +0,0 @@
package org.thoughtcrime.securesms.recipients.ui.bottomsheet;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.avatar.view.AvatarView;
import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment;
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar;
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon;
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference;
import org.thoughtcrime.securesms.fonts.SignalSymbols;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.nicknames.NicknameActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.recipients.ui.about.AboutSheet;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.WindowUtil;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import kotlin.Unit;
/**
* A bottom sheet that shows some simple recipient details, as well as some actions (like calling,
* adding to contacts, etc).
*/
public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogFragment {
public static final String TAG = Log.tag(RecipientBottomSheetDialogFragment.class);
public static final int REQUEST_CODE_SYSTEM_CONTACT_SHEET = 1111;
private static final String ARGS_RECIPIENT_ID = "RECIPIENT_ID";
private static final String ARGS_GROUP_ID = "GROUP_ID";
private RecipientDialogViewModel viewModel;
private AvatarView avatar;
private TextView fullName;
private TextView about;
private TextView nickname;
private TextView blockButton;
private TextView unblockButton;
private TextView addContactButton;
private TextView contactDetailsButton;
private TextView addToGroupButton;
private TextView viewSafetyNumberButton;
private TextView makeGroupAdminButton;
private TextView removeAdminButton;
private TextView removeFromGroupButton;
private ProgressBar adminActionBusy;
private View noteToSelfDescription;
private View buttonStrip;
private View interactionsContainer;
private BadgeImageView badgeImageView;
private Callback callback;
private ButtonStripPreference.ViewHolder buttonStripViewHolder;
private ActivityResultLauncher<NicknameActivity.Args> nicknameLauncher;
public static void show(FragmentManager fragmentManager, @NonNull RecipientId recipientId, @Nullable GroupId groupId) {
Recipient recipient = Recipient.resolved(recipientId);
if (recipient.isSelf()) {
AboutSheet.create(recipient).show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
} else {
Bundle args = new Bundle();
RecipientBottomSheetDialogFragment fragment = new RecipientBottomSheetDialogFragment();
args.putString(ARGS_RECIPIENT_ID, recipientId.serialize());
if (groupId != null) {
args.putString(ARGS_GROUP_ID, groupId.toString());
}
fragment.setArguments(args);
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
setStyle(DialogFragment.STYLE_NORMAL,
ThemeUtil.isDarkTheme(requireContext()) ? R.style.Theme_Signal_RoundedBottomSheet
: R.style.Theme_Signal_RoundedBottomSheet_Light);
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.recipient_bottom_sheet, container, false);
avatar = view.findViewById(R.id.rbs_recipient_avatar);
fullName = view.findViewById(R.id.rbs_full_name);
about = view.findViewById(R.id.rbs_about);
nickname = view.findViewById(R.id.rbs_nickname_button);
blockButton = view.findViewById(R.id.rbs_block_button);
unblockButton = view.findViewById(R.id.rbs_unblock_button);
addContactButton = view.findViewById(R.id.rbs_add_contact_button);
contactDetailsButton = view.findViewById(R.id.rbs_contact_details_button);
addToGroupButton = view.findViewById(R.id.rbs_add_to_group_button);
viewSafetyNumberButton = view.findViewById(R.id.rbs_view_safety_number_button);
makeGroupAdminButton = view.findViewById(R.id.rbs_make_group_admin_button);
removeAdminButton = view.findViewById(R.id.rbs_remove_group_admin_button);
removeFromGroupButton = view.findViewById(R.id.rbs_remove_from_group_button);
adminActionBusy = view.findViewById(R.id.rbs_admin_action_busy);
noteToSelfDescription = view.findViewById(R.id.rbs_note_to_self_description);
buttonStrip = view.findViewById(R.id.button_strip);
interactionsContainer = view.findViewById(R.id.interactions_container);
badgeImageView = view.findViewById(R.id.rbs_badge);
buttonStripViewHolder = new ButtonStripPreference.ViewHolder(buttonStrip);
return view;
}
@Override
public void onViewCreated(@NonNull View fragmentView, @Nullable Bundle savedInstanceState) {
super.onViewCreated(fragmentView, savedInstanceState);
nicknameLauncher = registerForActivityResult(new NicknameActivity.Contract(), (b) -> {});
Bundle arguments = requireArguments();
RecipientId recipientId = RecipientId.from(Objects.requireNonNull(arguments.getString(ARGS_RECIPIENT_ID)));
GroupId groupId = GroupId.parseNullableOrThrow(arguments.getString(ARGS_GROUP_ID));
RecipientDialogViewModel.Factory factory = new RecipientDialogViewModel.Factory(requireContext().getApplicationContext(), recipientId, groupId);
viewModel = new ViewModelProvider(this, factory).get(RecipientDialogViewModel.class);
viewModel.getStoryViewState().observe(getViewLifecycleOwner(), state -> {
avatar.setStoryRingFromState(state);
});
viewModel.getRecipient().observe(getViewLifecycleOwner(), recipient -> {
interactionsContainer.setVisibility(recipient.isSelf() ? View.GONE : View.VISIBLE);
avatar.displayChatAvatar(recipient);
if (!recipient.isSelf()) {
badgeImageView.setBadgeFromRecipient(recipient);
}
if (recipient.isSelf()) {
avatar.setOnClickListener(v -> {
dismiss();
viewModel.onNoteToSelfClicked(requireActivity());
});
}
String name = recipient.isSelf() ? requireContext().getString(R.string.note_to_self)
: recipient.getDisplayName(requireContext());
fullName.setVisibility(TextUtils.isEmpty(name) ? View.GONE : View.VISIBLE);
SpannableStringBuilder nameBuilder = new SpannableStringBuilder(name);
if (recipient.getShowVerified()) {
SpanUtil.appendSpacer(nameBuilder, 8);
SpanUtil.appendCenteredImageSpanWithoutSpace(nameBuilder, ContextUtil.requireDrawable(requireContext(), R.drawable.ic_official_28), 28, 28);
} else if (recipient.isSystemContact()) {
CharSequence systemContactGlyph = SignalSymbols.getSpannedString(requireContext(),
SignalSymbols.Weight.BOLD,
SignalSymbols.Glyph.PERSON_CIRCLE);
nameBuilder.append(" ");
nameBuilder.append(SpanUtil.ofSize(systemContactGlyph, 20));
}
if (!recipient.isSelf() && recipient.isIndividual()) {
CharSequence chevronGlyph = SignalSymbols.getSpannedString(requireContext(),
SignalSymbols.Weight.BOLD,
SignalSymbols.Glyph.CHEVRON_RIGHT);
nameBuilder.append(" ");
nameBuilder.append(SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.signal_colorOutline),
SpanUtil.ofSize(chevronGlyph, 24)));
fullName.setText(nameBuilder);
fullName.setOnClickListener(v -> {
dismiss();
AboutSheet.create(recipient).show(getParentFragmentManager(), null);
});
nickname.setVisibility(View.VISIBLE);
nickname.setOnClickListener(v -> {
nicknameLauncher.launch(new NicknameActivity.Args(
recipientId,
false
));
});
}
String aboutText = recipient.getCombinedAboutAndEmoji();
if (recipient.isReleaseNotes()) {
aboutText = getString(R.string.ReleaseNotes__signal_release_notes_and_news);
}
if (!Util.isEmpty(aboutText)) {
about.setText(aboutText);
about.setVisibility(View.VISIBLE);
} else {
about.setVisibility(View.GONE);
}
noteToSelfDescription.setVisibility(recipient.isSelf() ? View.VISIBLE : View.GONE);
if (RecipientUtil.isBlockable(recipient)) {
boolean blocked = recipient.isBlocked();
blockButton .setVisibility(recipient.isSelf() || blocked ? View.GONE : View.VISIBLE);
unblockButton.setVisibility(recipient.isSelf() || !blocked ? View.GONE : View.VISIBLE);
} else {
blockButton .setVisibility(View.GONE);
unblockButton.setVisibility(View.GONE);
}
boolean isAudioAvailable = recipient.isRegistered() &&
!recipient.isGroup() &&
!recipient.isBlocked() &&
!recipient.isSelf() &&
!recipient.isReleaseNotes();
ButtonStripPreference.State buttonStripState = new ButtonStripPreference.State(
/* isMessageAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && !recipient.isReleaseNotes(),
/* isVideoAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && recipient.isRegistered(),
/* isAudioAvailable = */ isAudioAvailable,
/* isMuteAvailable = */ false,
/* isSearchAvailable = */ false,
/* isAudioSecure = */ recipient.isRegistered(),
/* isMuted = */ false,
/* isAddToStoryAvailable = */ false
);
ButtonStripPreference.Model buttonStripModel = new ButtonStripPreference.Model(
buttonStripState,
DSLSettingsIcon.from(ContextUtil.requireDrawable(requireContext(), R.drawable.selectable_recipient_bottom_sheet_icon_button)),
!viewModel.isDeprecatedOrUnregistered(),
() -> Unit.INSTANCE,
() -> {
dismiss();
viewModel.onMessageClicked(requireActivity());
return Unit.INSTANCE;
},
() -> {
viewModel.onSecureVideoCallClicked(requireActivity(), () -> YouAreAlreadyInACallSnackbar.show(requireView()));
return Unit.INSTANCE;
},
() -> {
if (buttonStripState.isAudioSecure()) {
viewModel.onSecureCallClicked(requireActivity(), () -> YouAreAlreadyInACallSnackbar.show(requireView()));
} else {
viewModel.onInsecureCallClicked(requireActivity());
}
return Unit.INSTANCE;
},
() -> Unit.INSTANCE,
() -> Unit.INSTANCE
);
buttonStripViewHolder.bind(buttonStripModel);
if (recipient.isReleaseNotes()) {
buttonStrip.setVisibility(View.GONE);
}
if (recipient.isSystemContact() || recipient.isGroup() || recipient.isSelf() || recipient.isBlocked() || recipient.isReleaseNotes() || !recipient.getHasE164() || !recipient.getShouldShowE164()) {
addContactButton.setVisibility(View.GONE);
} else {
addContactButton.setVisibility(View.VISIBLE);
addContactButton.setOnClickListener(v -> {
openSystemContactSheet(RecipientExporter.export(recipient).asAddContactIntent());
});
}
if (recipient.isSystemContact() && !recipient.isGroup() && !recipient.isSelf()) {
contactDetailsButton.setVisibility(View.VISIBLE);
contactDetailsButton.setOnClickListener(v -> {
openSystemContactSheet(new Intent(Intent.ACTION_VIEW, recipient.getContactUri()));
});
} else {
contactDetailsButton.setVisibility(View.GONE);
}
});
viewModel.getCanAddToAGroup().observe(getViewLifecycleOwner(), canAdd -> {
addToGroupButton.setText(groupId == null ? R.string.RecipientBottomSheet_add_to_a_group : R.string.RecipientBottomSheet_add_to_another_group);
addToGroupButton.setVisibility(canAdd ? View.VISIBLE : View.GONE);
});
viewModel.getAdminActionStatus().observe(getViewLifecycleOwner(), adminStatus -> {
makeGroupAdminButton.setVisibility(adminStatus.isCanMakeAdmin() ? View.VISIBLE : View.GONE);
removeAdminButton.setVisibility(adminStatus.isCanMakeNonAdmin() ? View.VISIBLE : View.GONE);
removeFromGroupButton.setVisibility(adminStatus.isCanRemove() ? View.VISIBLE : View.GONE);
if (adminStatus.isCanRemove()) {
removeFromGroupButton.setOnClickListener(view -> viewModel.onRemoveFromGroupClicked(requireActivity(), adminStatus.isLinkActive(), this::dismiss));
}
});
viewModel.getIdentity().observe(getViewLifecycleOwner(), identityRecord -> {
if (identityRecord != null) {
viewSafetyNumberButton.setVisibility(View.VISIBLE);
viewSafetyNumberButton.setOnClickListener(view -> {
dismiss();
viewModel.onViewSafetyNumberClicked(requireActivity(), identityRecord);
});
}
});
avatar.setOnClickListener(view -> {
dismiss();
viewModel.onAvatarClicked(requireActivity());
});
badgeImageView.setOnClickListener(view -> {
dismiss();
ViewBadgeBottomSheetDialogFragment.show(getParentFragmentManager(), recipientId, null);
});
blockButton.setOnClickListener(view -> viewModel.onBlockClicked(requireActivity()));
unblockButton.setOnClickListener(view -> viewModel.onUnblockClicked(requireActivity()));
makeGroupAdminButton.setOnClickListener(view -> viewModel.onMakeGroupAdminClicked(requireActivity()));
removeAdminButton.setOnClickListener(view -> viewModel.onRemoveGroupAdminClicked(requireActivity()));
addToGroupButton.setOnClickListener(view -> {
dismiss();
viewModel.onAddToGroupButton(requireActivity());
});
viewModel.getAdminActionBusy().observe(getViewLifecycleOwner(), busy -> {
adminActionBusy.setVisibility(busy ? View.VISIBLE : View.GONE);
boolean userLoggedOut = viewModel.isDeprecatedOrUnregistered();
makeGroupAdminButton.setEnabled(!busy && !userLoggedOut);
removeAdminButton.setEnabled(!busy && !userLoggedOut);
removeFromGroupButton.setEnabled(!busy && !userLoggedOut);
});
callback = getParentFragment() != null && getParentFragment() instanceof Callback ? (Callback) getParentFragment() : null;
if (viewModel.isDeprecatedOrUnregistered()) {
List<TextView> viewsToDisable = Arrays.asList(blockButton, unblockButton, removeFromGroupButton, makeGroupAdminButton, removeAdminButton, addToGroupButton, viewSafetyNumberButton);
for (TextView view : viewsToDisable) {
view.setEnabled(false);
view.setAlpha(0.5f);
}
}
}
@Override
public void onResume() {
super.onResume();
WindowUtil.initializeScreenshotSecurity(requireContext(), requireDialog().getWindow());
}
private void openSystemContactSheet(@NonNull Intent intent) {
try {
startActivityForResult(intent, REQUEST_CODE_SYSTEM_CONTACT_SHEET);
} catch (ActivityNotFoundException e) {
Log.w(TAG, "No activity existed to open the contact.");
Toast.makeText(requireContext(), R.string.RecipientBottomSheet_unable_to_open_contacts, Toast.LENGTH_LONG).show();
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE_SYSTEM_CONTACT_SHEET) {
viewModel.refreshRecipient();
}
}
@Override
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
BottomSheetUtil.show(manager, tag, this);
}
@Override
public void onDismiss(@NonNull DialogInterface dialog) {
super.onDismiss(dialog);
if (callback != null) {
callback.onRecipientBottomSheetDismissed();
}
}
public interface Callback {
void onRecipientBottomSheetDismissed();
}
}

View File

@@ -0,0 +1,455 @@
package org.thoughtcrime.securesms.recipients.ui.bottomsheet
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.view.AvatarView
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache
import org.thoughtcrime.securesms.fonts.SignalSymbols
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.nicknames.NicknameActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientExporter
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientUtil
import org.thoughtcrime.securesms.recipients.ui.about.AboutSheet
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.ContextUtil
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.WindowUtil
import org.thoughtcrime.securesms.util.visible
/**
* A bottom sheet that shows some simple recipient details, as well as some actions (like calling,
* adding to contacts, etc).
*/
class RecipientBottomSheetDialogFragment : BottomSheetDialogFragment() {
companion object {
val TAG: String = Log.tag(RecipientBottomSheetDialogFragment::class.java)
const val REQUEST_CODE_SYSTEM_CONTACT_SHEET: Int = 1111
private const val ARGS_RECIPIENT_ID = "RECIPIENT_ID"
private const val ARGS_GROUP_ID = "GROUP_ID"
private const val LOADING_DELAY = 800L
private const val FADE_DURATION = 150L
@JvmStatic
fun show(fragmentManager: FragmentManager, recipientId: RecipientId, groupId: GroupId?) {
val recipient = Recipient.resolved(recipientId)
if (recipient.isSelf) {
AboutSheet.create(recipient).show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
} else {
val args = Bundle()
val fragment = RecipientBottomSheetDialogFragment()
args.putString(ARGS_RECIPIENT_ID, recipientId.serialize())
if (groupId != null) {
args.putString(ARGS_GROUP_ID, groupId.toString())
}
fragment.setArguments(args)
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}
private val viewModel: RecipientDialogViewModel by viewModels(factoryProducer = this::createFactory)
private var callback: Callback? = null
private fun createFactory(): RecipientDialogViewModel.Factory {
val arguments: Bundle = requireArguments()
val recipientId = RecipientId.from(arguments.getString(ARGS_RECIPIENT_ID)!!)
val groupId: GroupId? = GroupId.parseNullableOrThrow(arguments.getString(ARGS_GROUP_ID))
return RecipientDialogViewModel.Factory(requireContext(), recipientId, groupId)
}
override fun onCreate(savedInstanceState: Bundle?) {
setStyle(
DialogFragment.STYLE_NORMAL,
if (ThemeUtil.isDarkTheme(requireContext())) R.style.Theme_Signal_RoundedBottomSheet else R.style.Theme_Signal_RoundedBottomSheet_Light
)
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.recipient_bottom_sheet, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val avatar: AvatarView = view.findViewById(R.id.rbs_recipient_avatar)
val fullName: TextView = view.findViewById(R.id.rbs_full_name)
val about: TextView = view.findViewById(R.id.rbs_about)
val nickname: TextView = view.findViewById(R.id.rbs_nickname_button)
val blockButton: TextView = view.findViewById(R.id.rbs_block_button)
val unblockButton: TextView = view.findViewById(R.id.rbs_unblock_button)
val addContactButton: TextView = view.findViewById(R.id.rbs_add_contact_button)
val contactDetailsButton: TextView = view.findViewById(R.id.rbs_contact_details_button)
val addToGroupButton: TextView = view.findViewById(R.id.rbs_add_to_group_button)
val viewSafetyNumberButton: TextView = view.findViewById(R.id.rbs_view_safety_number_button)
val makeGroupAdminButton: TextView = view.findViewById(R.id.rbs_make_group_admin_button)
val removeAdminButton: TextView = view.findViewById(R.id.rbs_remove_group_admin_button)
val removeFromGroupButton: TextView = view.findViewById(R.id.rbs_remove_from_group_button)
val adminActionBusy: ProgressBar = view.findViewById(R.id.rbs_admin_action_busy)
val noteToSelfDescription: View = view.findViewById(R.id.rbs_note_to_self_description)
val buttonStrip: View = view.findViewById(R.id.button_strip)
val interactionsContainer: View = view.findViewById(R.id.interactions_container)
val badgeImageView: BadgeImageView = view.findViewById(R.id.rbs_badge)
val tapToView: View = view.findViewById(R.id.rbs_tap_to_view)
val progressBar: ProgressBar = view.findViewById(R.id.rbs_progress_bar)
val buttonStripViewHolder = ButtonStripPreference.ViewHolder(buttonStrip)
val nicknameLauncher = registerForActivityResult(NicknameActivity.Contract()) {}
val arguments = requireArguments()
val recipientId = RecipientId.from(arguments.getString(ARGS_RECIPIENT_ID)!!)
val groupId = GroupId.parseNullableOrThrow(arguments.getString(ARGS_GROUP_ID))
viewModel.storyViewState.observe(viewLifecycleOwner) { state ->
avatar.setStoryRingFromState(state)
}
var inProgress = false
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
val recipient = viewModel.recipient.value
if (recipient != null) {
AvatarDownloadStateCache.forRecipient(recipient.id).collect {
when (it) {
AvatarDownloadStateCache.DownloadState.NONE -> {}
AvatarDownloadStateCache.DownloadState.IN_PROGRESS -> {
if (inProgress) {
return@collect
}
inProgress = true
animateAvatarLoading(recipient, avatar)
tapToView.visible = false
tapToView.setOnClickListener(null)
delay(LOADING_DELAY)
progressBar.visible = AvatarDownloadStateCache.getDownloadState(recipient) == AvatarDownloadStateCache.DownloadState.IN_PROGRESS
}
AvatarDownloadStateCache.DownloadState.FINISHED -> {
AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.NONE)
viewModel.refreshGroupId(groupId)
inProgress = false
progressBar.visible = false
}
AvatarDownloadStateCache.DownloadState.FAILED -> {
AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.NONE)
avatar.displayGradientBlur(recipient)
viewModel.onResetBlurAvatar(recipient)
inProgress = false
progressBar.visible = false
Snackbar.make(view, R.string.ConversationFragment_photo_failed, Snackbar.LENGTH_LONG).show()
}
}
}
}
}
}
viewModel.recipient.observe(viewLifecycleOwner) { recipient ->
interactionsContainer.visible = !recipient.isSelf
if (AvatarDownloadStateCache.getDownloadState(recipient) != AvatarDownloadStateCache.DownloadState.IN_PROGRESS) {
avatar.displayChatAvatar(recipient)
}
if (!recipient.isSelf) {
badgeImageView.setBadgeFromRecipient(recipient)
}
if (recipient.isSelf) {
avatar.setOnClickListener {
dismiss()
viewModel.onNoteToSelfClicked(requireActivity())
}
}
if (recipient.shouldBlurAvatar && recipient.hasAvatar) {
tapToView.visible = true
tapToView.setOnClickListener {
AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.IN_PROGRESS)
viewModel.onTapToViewAvatar(recipient)
}
} else {
tapToView.visible = false
tapToView.setOnClickListener(null)
}
val name = if (recipient.isSelf) requireContext().getString(R.string.note_to_self) else recipient.getDisplayName(requireContext())
fullName.visible = name.isNotEmpty()
val nameBuilder = SpannableStringBuilder(name)
if (recipient.showVerified) {
SpanUtil.appendSpacer(nameBuilder, 8)
SpanUtil.appendCenteredImageSpanWithoutSpace(nameBuilder, ContextUtil.requireDrawable(requireContext(), R.drawable.ic_official_28), 28, 28)
} else if (recipient.isSystemContact) {
val systemContactGlyph = SignalSymbols.getSpannedString(
requireContext(),
SignalSymbols.Weight.BOLD,
SignalSymbols.Glyph.PERSON_CIRCLE
)
nameBuilder.append(" ")
nameBuilder.append(SpanUtil.ofSize(systemContactGlyph, 20))
}
if (!recipient.isSelf && recipient.isIndividual) {
val chevronGlyph = SignalSymbols.getSpannedString(
requireContext(),
SignalSymbols.Weight.BOLD,
SignalSymbols.Glyph.CHEVRON_RIGHT
)
nameBuilder.append(" ")
nameBuilder.append(
SpanUtil.color(ContextCompat.getColor(requireContext(), R.color.signal_colorOutline), SpanUtil.ofSize(chevronGlyph, 24))
)
fullName.text = nameBuilder
fullName.setOnClickListener {
dismiss()
AboutSheet.create(recipient).show(getParentFragmentManager(), null)
}
nickname.visible = true
nickname.setOnClickListener {
nicknameLauncher.launch(NicknameActivity.Args(recipientId, false))
}
}
var aboutText = recipient.combinedAboutAndEmoji
if (recipient.isReleaseNotes) {
aboutText = getString(R.string.ReleaseNotes__signal_release_notes_and_news)
}
if (!aboutText.isNullOrEmpty()) {
about.text = aboutText
about.visible = true
} else {
about.visible = false
}
noteToSelfDescription.visible = recipient.isSelf
if (RecipientUtil.isBlockable(recipient)) {
val blocked = recipient.isBlocked
blockButton.visible = !recipient.isSelf && !blocked
unblockButton.visible = !recipient.isSelf && blocked
} else {
blockButton.visible = false
unblockButton.visible = false
}
val isAudioAvailable = recipient.isRegistered &&
!recipient.isGroup &&
!recipient.isBlocked &&
!recipient.isSelf &&
!recipient.isReleaseNotes
val buttonStripState = ButtonStripPreference.State(
isMessageAvailable = !recipient.isBlocked && !recipient.isSelf && !recipient.isReleaseNotes,
isVideoAvailable = !recipient.isBlocked && !recipient.isSelf && recipient.isRegistered,
isAudioAvailable = isAudioAvailable,
isAudioSecure = recipient.isRegistered
)
val buttonStripModel = ButtonStripPreference.Model(
state = buttonStripState,
background = DSLSettingsIcon.from(ContextUtil.requireDrawable(requireContext(), R.drawable.selectable_recipient_bottom_sheet_icon_button)),
enabled = !viewModel.isDeprecatedOrUnregistered,
onMessageClick = {
dismiss()
viewModel.onMessageClicked(requireActivity())
},
onVideoClick = {
viewModel.onSecureVideoCallClicked(requireActivity()) { YouAreAlreadyInACallSnackbar.show(requireView()) }
},
onAudioClick = {
if (buttonStripState.isAudioSecure) {
viewModel.onSecureCallClicked(requireActivity()) { YouAreAlreadyInACallSnackbar.show(requireView()) }
} else {
viewModel.onInsecureCallClicked(requireActivity())
}
}
)
buttonStripViewHolder.bind(buttonStripModel)
if (recipient.isReleaseNotes) {
buttonStrip.visible = false
}
if (recipient.isSystemContact || recipient.isGroup || recipient.isSelf || recipient.isBlocked || recipient.isReleaseNotes || !recipient.hasE164 || !recipient.shouldShowE164) {
addContactButton.visible = false
} else {
addContactButton.visible = true
addContactButton.setOnClickListener {
openSystemContactSheet(RecipientExporter.export(recipient).asAddContactIntent())
}
}
if (recipient.isSystemContact && !recipient.isGroup && !recipient.isSelf) {
contactDetailsButton.visible = true
contactDetailsButton.setOnClickListener {
openSystemContactSheet(Intent(Intent.ACTION_VIEW, recipient.contactUri))
}
} else {
contactDetailsButton.visible = false
}
}
viewModel.canAddToAGroup.observe(getViewLifecycleOwner()) { canAdd: Boolean ->
addToGroupButton.setText(if (groupId == null) R.string.RecipientBottomSheet_add_to_a_group else R.string.RecipientBottomSheet_add_to_another_group)
addToGroupButton.visible = canAdd
}
viewModel.adminActionStatus.observe(viewLifecycleOwner) { adminStatus ->
makeGroupAdminButton.visible = adminStatus.isCanMakeAdmin
removeAdminButton.visible = adminStatus.isCanMakeNonAdmin
removeFromGroupButton.visible = adminStatus.isCanRemove
if (adminStatus.isCanRemove) {
removeFromGroupButton.setOnClickListener { viewModel.onRemoveFromGroupClicked(requireActivity(), adminStatus.isLinkActive) { this.dismiss() } }
}
}
viewModel.identity.observe(viewLifecycleOwner) { identityRecord ->
if (identityRecord != null) {
viewSafetyNumberButton.visible = true
viewSafetyNumberButton.setOnClickListener {
dismiss()
viewModel.onViewSafetyNumberClicked(requireActivity(), identityRecord)
}
}
}
avatar.setOnClickListener {
dismiss()
viewModel.onAvatarClicked(requireActivity())
}
badgeImageView.setOnClickListener {
dismiss()
ViewBadgeBottomSheetDialogFragment.show(getParentFragmentManager(), recipientId, null)
}
blockButton.setOnClickListener { viewModel.onBlockClicked(requireActivity()) }
unblockButton.setOnClickListener { viewModel.onUnblockClicked(requireActivity()) }
makeGroupAdminButton.setOnClickListener { viewModel.onMakeGroupAdminClicked(requireActivity()) }
removeAdminButton.setOnClickListener { viewModel.onRemoveGroupAdminClicked(requireActivity()) }
addToGroupButton.setOnClickListener {
dismiss()
viewModel.onAddToGroupButton(requireActivity())
}
viewModel.adminActionBusy.observe(viewLifecycleOwner) { busy ->
adminActionBusy.visible = busy
val userLoggedOut = viewModel.isDeprecatedOrUnregistered
makeGroupAdminButton.isEnabled = !busy && !userLoggedOut
removeAdminButton.isEnabled = !busy && !userLoggedOut
removeFromGroupButton.isEnabled = !busy && !userLoggedOut
}
callback = if (parentFragment != null && parentFragment is Callback) parentFragment as Callback else null
if (viewModel.isDeprecatedOrUnregistered) {
val viewsToDisable = listOf(blockButton, unblockButton, removeFromGroupButton, makeGroupAdminButton, removeAdminButton, addToGroupButton, viewSafetyNumberButton)
for (textView in viewsToDisable) {
textView.isEnabled = false
textView.alpha = 0.5f
}
}
}
override fun onResume() {
super.onResume()
WindowUtil.initializeScreenshotSecurity(requireContext(), requireDialog().window!!)
}
private fun openSystemContactSheet(intent: Intent) {
try {
startActivityForResult(intent, REQUEST_CODE_SYSTEM_CONTACT_SHEET)
} catch (e: ActivityNotFoundException) {
Log.w(TAG, "No activity existed to open the contact.")
Toast.makeText(requireContext(), R.string.RecipientBottomSheet_unable_to_open_contacts, Toast.LENGTH_LONG).show()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE_SYSTEM_CONTACT_SHEET) {
viewModel.refreshRecipient()
}
}
override fun show(manager: FragmentManager, tag: String?) {
BottomSheetUtil.show(manager, tag, this)
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
if (callback != null) {
callback!!.onRecipientBottomSheetDismissed()
}
}
private fun animateAvatarLoading(recipient: Recipient, avatar: AvatarView) {
val animator = ObjectAnimator.ofFloat(avatar, "alpha", 1f, 0f).setDuration(FADE_DURATION)
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
if (AvatarDownloadStateCache.getDownloadState(recipient) == AvatarDownloadStateCache.DownloadState.IN_PROGRESS) {
avatar.displayLoadingAvatar()
}
ObjectAnimator.ofFloat(avatar, "alpha", 0f, 1f).setDuration(FADE_DURATION).start()
}
})
animator.start()
}
interface Callback {
fun onRecipientBottomSheetDismissed()
}
}

View File

@@ -17,11 +17,13 @@ import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.libsignal.protocol.util.Pair;
import org.thoughtcrime.securesms.BlockUnblockDialog;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
import org.thoughtcrime.securesms.database.GroupTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.thoughtcrime.securesms.database.model.StoryViewState;
import org.thoughtcrime.securesms.groups.GroupId;
@@ -29,6 +31,8 @@ import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity;
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -265,6 +269,28 @@ final class RecipientDialogViewModel extends ViewModel {
ThreadUtil.runOnMain(() -> Toast.makeText(context, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show());
}
public void onTapToViewAvatar(@NonNull Recipient recipient) {
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.recipients().manuallyUpdateShowAvatar(recipient.getId(), true));
if (recipient.isPushV2Group()) {
AvatarGroupsV2DownloadJob.enqueueUnblurredAvatar(recipient.requireGroupId().requireV2());
} else {
RetrieveProfileAvatarJob.enqueueUnblurredAvatar(recipient);
}
}
public void onResetBlurAvatar(@NonNull Recipient recipient) {
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.recipients().manuallyUpdateShowAvatar(recipient.getId(), false));
}
public void refreshGroupId(@Nullable GroupId groupId) {
if (groupId != null) {
SignalExecutors.BOUNDED.execute(() -> {
RecipientId groupRecipientId = SignalDatabase.groups().getGroup(groupId).get().getRecipientId();
Recipient.live(groupRecipientId).refresh();
});
}
}
static class AdminActionStatus {
private final boolean canRemove;
private final boolean canMakeAdmin;