mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-20 11:08:31 +00:00
Block avatar downloads in message request states.
This commit is contained in:
@@ -4,10 +4,12 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.res.use
|
||||
import com.bumptech.glide.RequestManager
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarGradientColors.getGradientDrawable
|
||||
import org.thoughtcrime.securesms.database.model.StoryViewState
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.stories.Stories
|
||||
@@ -76,8 +78,16 @@ class AvatarView @JvmOverloads constructor(
|
||||
/**
|
||||
* Displays Note-to-Self
|
||||
*/
|
||||
fun displayChatAvatar(requestManager: RequestManager, recipient: Recipient, isQuickContactEnabled: Boolean) {
|
||||
avatar.setAvatar(requestManager, recipient, isQuickContactEnabled)
|
||||
fun displayChatAvatar(requestManager: RequestManager, recipient: Recipient, isQuickContactEnabled: Boolean, useBlurGradient: Boolean) {
|
||||
avatar.setAvatar(requestManager, recipient, isQuickContactEnabled, false, useBlurGradient)
|
||||
}
|
||||
|
||||
fun displayLoadingAvatar() {
|
||||
avatar.setImageDrawable(AppCompatResources.getDrawable(context, R.drawable.circle_profile_photo))
|
||||
}
|
||||
|
||||
fun displayGradientBlur(recipient: Recipient) {
|
||||
avatar.setImageDrawable(getGradientDrawable(recipient))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
@@ -25,14 +24,11 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.load.resource.bitmap.CircleCrop;
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.SimpleTarget;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.google.android.material.imageview.ShapeableImageView;
|
||||
import com.google.android.material.shape.RelativeCornerSize;
|
||||
import com.google.android.material.shape.RoundedCornerTreatment;
|
||||
import com.google.android.material.shape.ShapeAppearanceModel;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -41,17 +37,18 @@ import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable;
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarGradientColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
import org.thoughtcrime.securesms.util.BlurTransformation;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -157,9 +154,14 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
}
|
||||
|
||||
public void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar) {
|
||||
setAvatar(requestManager, recipient, quickContactEnabled, useSelfProfileAvatar, false);
|
||||
}
|
||||
|
||||
public void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar, boolean useBlurGradient) {
|
||||
setAvatar(requestManager, recipient, new AvatarOptions.Builder(this)
|
||||
.withUseSelfProfileAvatar(useSelfProfileAvatar)
|
||||
.withQuickContactEnabled(quickContactEnabled)
|
||||
.withUseBlurGradient(useBlurGradient)
|
||||
.build());
|
||||
}
|
||||
|
||||
@@ -195,23 +197,21 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
};
|
||||
}
|
||||
|
||||
Drawable fallback = new FallbackAvatarDrawable(getContext(), activeFallbackPhotoProvider.getFallbackAvatar(recipient)).circleCrop();
|
||||
boolean wasUnblurred = shouldBlur != blurred;
|
||||
boolean inProgressDownload = AvatarDownloadStateCache.getDownloadState(recipient) == AvatarDownloadStateCache.DownloadState.IN_PROGRESS;
|
||||
boolean shouldShowBlurGradient = avatarOptions.useBlurGradient && (!recipient.getShouldShowAvatarByDefault() || inProgressDownload);
|
||||
boolean shouldHaveAvatar = recipient.getHasAvatar();
|
||||
boolean hasAvatar = AvatarHelper.hasAvatar(getContext(), recipient.getId()) || (photo.contactPhoto instanceof SystemContactPhoto);
|
||||
Drawable fallback = new FallbackAvatarDrawable(getContext(), activeFallbackPhotoProvider.getFallbackAvatar(recipient)).circleCrop();
|
||||
|
||||
if (fixedSizeTarget != null) {
|
||||
requestManager.clear(fixedSizeTarget);
|
||||
}
|
||||
|
||||
if (photo.contactPhoto != null) {
|
||||
|
||||
List<Transformation<Bitmap>> transforms = new ArrayList<>();
|
||||
if (shouldBlur) {
|
||||
transforms.add(new BlurTransformation(AppDependencies.getApplication(), 0.25f, BlurTransformation.MAX_RADIUS));
|
||||
}
|
||||
transforms.add(new CircleCrop());
|
||||
blurred = shouldBlur;
|
||||
if (photo.contactPhoto != null && hasAvatar && !shouldBlur) {
|
||||
List<Transformation<Bitmap>> transforms = Collections.singletonList(new CircleCrop());
|
||||
|
||||
RequestBuilder<Drawable> request = requestManager.load(photo.contactPhoto)
|
||||
.dontAnimate()
|
||||
.fallback(fallback)
|
||||
.error(fallback)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
@@ -219,6 +219,13 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
.transform(new MultiTransformation<>(transforms))
|
||||
.addListener(redownloadRequestListener);
|
||||
|
||||
if (wasUnblurred) {
|
||||
blurred = shouldBlur;
|
||||
request = request.transition(DrawableTransitionOptions.withCrossFade(200));
|
||||
} else {
|
||||
request = request.dontAnimate();
|
||||
}
|
||||
|
||||
if (avatarOptions.fixedSize > 0) {
|
||||
fixedSizeTarget = new FixedSizeTarget(avatarOptions.fixedSize);
|
||||
request.into(fixedSizeTarget);
|
||||
@@ -226,7 +233,13 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
request.into(this);
|
||||
}
|
||||
|
||||
} else if ((shouldBlur || shouldShowBlurGradient) && shouldHaveAvatar) {
|
||||
setImageDrawable(AvatarGradientColors.getGradientDrawable(recipient));
|
||||
blurred = true;
|
||||
} else {
|
||||
if (!shouldBlur && shouldHaveAvatar && !hasAvatar && recipient.isIndividual()) {
|
||||
RetrieveProfileAvatarJob.enqueueForceUpdate(recipient);
|
||||
}
|
||||
setImageDrawable(fallback);
|
||||
}
|
||||
}
|
||||
@@ -336,11 +349,13 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
private final boolean quickContactEnabled;
|
||||
private final boolean useSelfProfileAvatar;
|
||||
private final boolean useBlurGradient;
|
||||
private final int fixedSize;
|
||||
|
||||
private AvatarOptions(@NonNull Builder builder) {
|
||||
this.quickContactEnabled = builder.quickContactEnabled;
|
||||
this.useSelfProfileAvatar = builder.useSelfProfileAvatar;
|
||||
this.useBlurGradient = builder.useBlurGradient;
|
||||
this.fixedSize = builder.fixedSize;
|
||||
}
|
||||
|
||||
@@ -350,6 +365,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
|
||||
private boolean quickContactEnabled = false;
|
||||
private boolean useSelfProfileAvatar = false;
|
||||
private boolean useBlurGradient = false;
|
||||
private int fixedSize = -1;
|
||||
|
||||
private Builder(@NonNull AvatarImageView avatarImageView) {
|
||||
@@ -366,6 +382,11 @@ public final class AvatarImageView extends AppCompatImageView {
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withUseBlurGradient(boolean useBlurGradient) {
|
||||
this.useBlurGradient = useBlurGradient;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withFixedSize(@Px @IntRange(from = 1) int fixedSize) {
|
||||
this.fixedSize = fixedSize;
|
||||
return this;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
@@ -13,6 +17,7 @@ import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewKt;
|
||||
@@ -21,11 +26,16 @@ import com.bumptech.glide.RequestManager;
|
||||
|
||||
import org.signal.core.util.DimensionUnit;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarGradientColors;
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.databinding.ConversationHeaderViewBinding;
|
||||
import org.thoughtcrime.securesms.fonts.SignalSymbols;
|
||||
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
@@ -34,8 +44,15 @@ import org.whispersystems.signalservice.api.util.Preconditions;
|
||||
|
||||
public class ConversationHeaderView extends ConstraintLayout {
|
||||
|
||||
private static final String TAG = Log.tag(ConversationHeaderView.class);
|
||||
private static final int FADE_DURATION = 150;
|
||||
private static final int LOADING_DELAY = 800;
|
||||
|
||||
private final ConversationHeaderViewBinding binding;
|
||||
|
||||
private boolean inProgress = false;
|
||||
private Handler handler = new Handler();
|
||||
|
||||
public ConversationHeaderView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
@@ -52,6 +69,30 @@ public class ConversationHeaderView extends ConstraintLayout {
|
||||
binding = ConversationHeaderViewBinding.bind(this);
|
||||
}
|
||||
|
||||
public void showProgressBar(@NonNull Recipient recipient) {
|
||||
if (!inProgress) {
|
||||
inProgress = true;
|
||||
animateAvatarLoading(recipient);
|
||||
binding.messageRequestAvatarTapToView.setVisibility(GONE);
|
||||
binding.messageRequestAvatarTapToView.setOnClickListener(null);
|
||||
handler.postDelayed(() -> {
|
||||
boolean isDownloading = AvatarDownloadStateCache.getDownloadState(recipient) == AvatarDownloadStateCache.DownloadState.IN_PROGRESS;
|
||||
binding.progressBar.setVisibility(isDownloading ? View.VISIBLE : View.GONE);
|
||||
}, LOADING_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
public void hideProgressBar() {
|
||||
inProgress = false;
|
||||
binding.progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void showFailedAvatarDownload(@NonNull Recipient recipient) {
|
||||
AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.NONE);
|
||||
binding.progressBar.setVisibility(View.GONE);
|
||||
binding.messageRequestAvatar.setImageDrawable(AvatarGradientColors.getGradientDrawable(recipient));
|
||||
}
|
||||
|
||||
public void setBadge(@Nullable Recipient recipient) {
|
||||
if (recipient == null || recipient.isSelf()) {
|
||||
binding.messageRequestBadge.setBadge(null);
|
||||
@@ -61,12 +102,25 @@ public class ConversationHeaderView extends ConstraintLayout {
|
||||
}
|
||||
|
||||
public void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient) {
|
||||
binding.messageRequestAvatar.setAvatar(requestManager, recipient, false);
|
||||
if (recipient == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (recipient != null && recipient.getShouldBlurAvatar() && recipient.getContactPhoto() != null) {
|
||||
if (AvatarDownloadStateCache.getDownloadState(recipient) != AvatarDownloadStateCache.DownloadState.IN_PROGRESS) {
|
||||
binding.messageRequestAvatar.setAvatar(requestManager, recipient, false, false, true);
|
||||
hideProgressBar();
|
||||
}
|
||||
|
||||
if (recipient.getShouldBlurAvatar() && recipient.getHasAvatar()) {
|
||||
binding.messageRequestAvatarTapToView.setVisibility(VISIBLE);
|
||||
binding.messageRequestAvatarTapToView.setOnClickListener(v -> {
|
||||
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.recipients().manuallyShowAvatar(recipient.getId()));
|
||||
AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.IN_PROGRESS);
|
||||
SignalExecutors.BOUNDED.execute(() -> SignalDatabase.recipients().manuallyUpdateShowAvatar(recipient.getId(), true));
|
||||
if (recipient.isPushV2Group()) {
|
||||
AvatarGroupsV2DownloadJob.enqueueUnblurredAvatar(recipient.requireGroupId().requireV2());
|
||||
} else {
|
||||
RetrieveProfileAvatarJob.enqueueUnblurredAvatar(recipient);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
binding.messageRequestAvatarTapToView.setVisibility(GONE);
|
||||
@@ -209,6 +263,22 @@ public class ConversationHeaderView extends ConstraintLayout {
|
||||
binding.messageRequestDescription.setMovementMethod(enable ? LongClickMovementMethod.getInstance(getContext()) : null);
|
||||
}
|
||||
|
||||
private void animateAvatarLoading(@NonNull Recipient recipient) {
|
||||
Drawable loadingProfile = AppCompatResources.getDrawable(getContext(), R.drawable.circle_profile_photo);
|
||||
ObjectAnimator animator = ObjectAnimator.ofFloat(binding.messageRequestAvatar, "alpha", 1f, 0f).setDuration(FADE_DURATION);
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (AvatarDownloadStateCache.getDownloadState(recipient) == AvatarDownloadStateCache.DownloadState.IN_PROGRESS) {
|
||||
binding.messageRequestAvatar.setImageDrawable(loadingProfile);
|
||||
}
|
||||
ObjectAnimator.ofFloat(binding.messageRequestAvatar, "alpha", 0f, 1f).setDuration(FADE_DURATION).start();
|
||||
}
|
||||
});
|
||||
|
||||
animator.start();
|
||||
}
|
||||
|
||||
private void updateOutlineVisibility() {
|
||||
if (ViewKt.isVisible(binding.messageRequestSubtitle) || ViewKt.isVisible(binding.messageRequestDescription)) {
|
||||
if (getBackground() != null) {
|
||||
|
||||
@@ -142,7 +142,7 @@ public class ConversationTitleView extends ConstraintLayout {
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(startDrawable, null, endDrawable, null);
|
||||
|
||||
if (recipient != null) {
|
||||
this.avatar.displayChatAvatar(requestManager, recipient, false);
|
||||
this.avatar.displayChatAvatar(requestManager, recipient, false, true);
|
||||
}
|
||||
|
||||
if (recipient == null || recipient.isSelf()) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.thoughtcrime.securesms.conversation.colors
|
||||
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import androidx.annotation.ColorInt
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* Lists gradients used to hide profiles during message request states
|
||||
*/
|
||||
object AvatarGradientColors {
|
||||
|
||||
@JvmStatic
|
||||
fun getGradientDrawable(recipient: Recipient): GradientDrawable {
|
||||
return if (recipient.serviceId.getOrNull() != null) {
|
||||
gradients[abs(recipient.requireServiceId().hashCode() % gradients.size)].getDrawable()
|
||||
} else if (recipient.groupId.getOrNull() != null) {
|
||||
gradients[abs(recipient.requireGroupId().hashCode() % gradients.size)].getDrawable()
|
||||
} else {
|
||||
gradients[0].getDrawable()
|
||||
}
|
||||
}
|
||||
|
||||
private val gradients = listOf(
|
||||
AvatarGradientColor(0xFF252568.toInt(), 0xFF9C8F8F.toInt()),
|
||||
AvatarGradientColor(0xFF2A4275.toInt(), 0xFF9D9EA1.toInt()),
|
||||
AvatarGradientColor(0xFF2E4B5F.toInt(), 0xFF8AA9B1.toInt()),
|
||||
AvatarGradientColor(0xFF2E426C.toInt(), 0xFF7A9377.toInt()),
|
||||
AvatarGradientColor(0xFF1A341A.toInt(), 0xFF807F6E.toInt()),
|
||||
AvatarGradientColor(0xFF464E42.toInt(), 0xFFD5C38F.toInt()),
|
||||
AvatarGradientColor(0xFF595643.toInt(), 0xFF93A899.toInt()),
|
||||
AvatarGradientColor(0xFF2C2F36.toInt(), 0xFF687466.toInt()),
|
||||
AvatarGradientColor(0xFF2B1E18.toInt(), 0xFF968980.toInt()),
|
||||
AvatarGradientColor(0xFF7B7067.toInt(), 0xFFA5A893.toInt()),
|
||||
AvatarGradientColor(0xFF706359.toInt(), 0xFFBDA194.toInt()),
|
||||
AvatarGradientColor(0xFF383331.toInt(), 0xFFA48788.toInt()),
|
||||
AvatarGradientColor(0xFF924F4F.toInt(), 0xFF897A7A.toInt()),
|
||||
AvatarGradientColor(0xFF663434.toInt(), 0xFFC58D77.toInt()),
|
||||
AvatarGradientColor(0xFF8F4B02.toInt(), 0xFFAA9274.toInt()),
|
||||
AvatarGradientColor(0xFF784747.toInt(), 0xFF8C8F6F.toInt()),
|
||||
AvatarGradientColor(0xFF747474.toInt(), 0xFFACACAC.toInt()),
|
||||
AvatarGradientColor(0xFF49484C.toInt(), 0xFFA5A6B5.toInt()),
|
||||
AvatarGradientColor(0xFF4A4E4D.toInt(), 0xFFABAFAE.toInt()),
|
||||
AvatarGradientColor(0xFF3A3A3A.toInt(), 0xFF929887.toInt())
|
||||
)
|
||||
|
||||
data class AvatarGradientColor(@ColorInt val startGradient: Int, @ColorInt val endGradient: Int) {
|
||||
fun getDrawable(): GradientDrawable {
|
||||
val gradientDrawable = GradientDrawable(
|
||||
GradientDrawable.Orientation.TOP_BOTTOM,
|
||||
intArrayOf(startGradient, endGradient)
|
||||
)
|
||||
gradientDrawable.shape = GradientDrawable.OVAL
|
||||
|
||||
return gradientDrawable
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.conversation.colors.Colorizable
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselectable
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationUpdate
|
||||
@@ -536,7 +537,19 @@ class ConversationAdapterV2(
|
||||
val (recipient, groupInfo, sharedGroups, messageRequestState) = model.recipientInfo
|
||||
val isSelf = recipient.id == Recipient.self().id
|
||||
|
||||
conversationBanner.setAvatar(requestManager, recipient)
|
||||
when (model.avatarDownloadState) {
|
||||
AvatarDownloadStateCache.DownloadState.NONE,
|
||||
AvatarDownloadStateCache.DownloadState.FINISHED -> {
|
||||
conversationBanner.setAvatar(requestManager, recipient)
|
||||
}
|
||||
AvatarDownloadStateCache.DownloadState.IN_PROGRESS -> {
|
||||
conversationBanner.showProgressBar(recipient)
|
||||
}
|
||||
AvatarDownloadStateCache.DownloadState.FAILED -> {
|
||||
conversationBanner.showFailedAvatarDownload(recipient)
|
||||
}
|
||||
}
|
||||
|
||||
conversationBanner.showBackgroundBubble(recipient.hasWallpaper)
|
||||
val title: String = conversationBanner.setTitle(recipient) {
|
||||
displayDialogFragment(AboutSheet.create(recipient))
|
||||
|
||||
@@ -67,6 +67,7 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import androidx.recyclerview.widget.ConversationLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -202,6 +203,7 @@ import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryReplace
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryResultsControllerV2
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryViewModelV2
|
||||
import org.thoughtcrime.securesms.conversation.v2.computed.ConversationMessageComputeWorkers
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement
|
||||
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupCallViewModel
|
||||
import org.thoughtcrime.securesms.conversation.v2.groups.ConversationGroupViewModel
|
||||
@@ -1056,6 +1058,28 @@ class ConversationFragment :
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
val recipient = viewModel.recipientSnapshot
|
||||
if (recipient != null) {
|
||||
AvatarDownloadStateCache.forRecipient(recipient.id).collect {
|
||||
when (it) {
|
||||
AvatarDownloadStateCache.DownloadState.NONE,
|
||||
AvatarDownloadStateCache.DownloadState.IN_PROGRESS,
|
||||
AvatarDownloadStateCache.DownloadState.FINISHED -> {
|
||||
viewModel.updateThreadHeader()
|
||||
}
|
||||
AvatarDownloadStateCache.DownloadState.FAILED -> {
|
||||
Snackbar.make(requireView(), R.string.ConversationFragment_photo_failed, Snackbar.LENGTH_LONG).show()
|
||||
presentConversationTitle(recipient)
|
||||
viewModel.onAvatarDownloadFailed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (TextSecurePreferences.getServiceOutage(context)) {
|
||||
AppDependencies.jobManager.add(ServiceOutageDetectionJob())
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bumptech.glide.RequestManager
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy
|
||||
@@ -36,6 +37,7 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.asFlow
|
||||
import org.signal.core.util.orNull
|
||||
import org.signal.paging.ProxyPagingController
|
||||
@@ -55,6 +57,7 @@ import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
|
||||
import org.thoughtcrime.securesms.conversation.v2.items.ChatColorsDrawable
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.MessageTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord
|
||||
import org.thoughtcrime.securesms.database.model.IdentityRecord
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
@@ -310,6 +313,20 @@ class ConversationViewModel(
|
||||
})
|
||||
}
|
||||
|
||||
fun onAvatarDownloadFailed() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val recipient = recipientSnapshot
|
||||
if (recipient != null) {
|
||||
recipients.manuallyUpdateShowAvatar(recipient.id, false)
|
||||
}
|
||||
pagingController.onDataItemChanged(ConversationElementKey.threadHeader)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateThreadHeader() {
|
||||
pagingController.onDataItemChanged(ConversationElementKey.threadHeader)
|
||||
}
|
||||
|
||||
fun getBannerFlows(
|
||||
context: Context,
|
||||
groupJoinClickListener: () -> Unit,
|
||||
|
||||
@@ -4,9 +4,14 @@ import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.Result
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
|
||||
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
@@ -29,8 +34,24 @@ class MessageRequestViewModel(
|
||||
fun onAccept(): Single<Result<Unit, GroupChangeFailureReason>> {
|
||||
return recipientId
|
||||
.flatMap { recipientId ->
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
if (recipient.isPushV2Group) {
|
||||
if (recipient.shouldBlurAvatar && recipient.hasAvatar) {
|
||||
AvatarGroupsV2DownloadJob.enqueueUnblurredAvatar(recipient.requireGroupId().requireV2())
|
||||
}
|
||||
|
||||
val jobs = recipient.participantIds
|
||||
.map { Recipient.resolved(it) }
|
||||
.filter { it.shouldBlurAvatar && it.hasAvatar }
|
||||
.map { RetrieveProfileAvatarJob(it, it.profileAvatar, true, true) }
|
||||
AppDependencies.jobManager.addAll(jobs)
|
||||
} else if (recipient.shouldBlurAvatar && recipient.hasAvatar) {
|
||||
RetrieveProfileAvatarJob.enqueueUnblurredAvatar(recipient)
|
||||
}
|
||||
|
||||
messageRequestRepository.acceptMessageRequest(recipientId, threadId)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.thoughtcrime.securesms.conversation.v2.data
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* Cache used to store the progress of both 1:1 and group avatar downloads
|
||||
*/
|
||||
object AvatarDownloadStateCache {
|
||||
|
||||
private val TAG = Log.tag(AvatarDownloadStateCache::class.java)
|
||||
|
||||
private val cache = HashMap<RecipientId, MutableStateFlow<DownloadState>>(100)
|
||||
|
||||
@JvmStatic
|
||||
fun set(recipient: Recipient, downloadState: DownloadState) {
|
||||
if (cache[recipient.id] == null) {
|
||||
cache[recipient.id] = MutableStateFlow(downloadState)
|
||||
} else {
|
||||
cache[recipient.id]!!.update { downloadState }
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDownloadState(recipient: Recipient): DownloadState {
|
||||
return cache[recipient.id]?.value ?: DownloadState.NONE
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forRecipient(id: RecipientId): StateFlow<DownloadState> {
|
||||
if (cache[id] == null) {
|
||||
cache[id] = MutableStateFlow(DownloadState.NONE)
|
||||
}
|
||||
|
||||
return cache[id]!!.asStateFlow()
|
||||
}
|
||||
|
||||
enum class DownloadState {
|
||||
NONE,
|
||||
IN_PROGRESS,
|
||||
FINISHED,
|
||||
FAILED
|
||||
}
|
||||
}
|
||||
@@ -217,7 +217,7 @@ class ConversationDataSource(
|
||||
}
|
||||
|
||||
private fun loadThreadHeader(): ThreadHeader {
|
||||
return ThreadHeader(messageRequestRepository.getRecipientInfo(threadRecipient.id, threadId))
|
||||
return ThreadHeader(messageRequestRepository.getRecipientInfo(threadRecipient.id, threadId), AvatarDownloadStateCache.getDownloadState(threadRecipient))
|
||||
}
|
||||
|
||||
private fun ConversationMessage.toMappingModel(): MappingModel<*> {
|
||||
|
||||
@@ -73,7 +73,7 @@ data class IncomingMedia(
|
||||
}
|
||||
}
|
||||
|
||||
data class ThreadHeader(val recipientInfo: MessageRequestRecipientInfo) : MappingModel<ThreadHeader> {
|
||||
data class ThreadHeader(val recipientInfo: MessageRequestRecipientInfo, val avatarDownloadState: AvatarDownloadStateCache.DownloadState) : MappingModel<ThreadHeader> {
|
||||
override fun areItemsTheSame(newItem: ThreadHeader): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1859,7 +1859,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
}
|
||||
}
|
||||
|
||||
fun setProfileAvatar(id: RecipientId, profileAvatar: String?) {
|
||||
fun setProfileAvatar(id: RecipientId, profileAvatar: String?, forceNotify: Boolean = false) {
|
||||
val contentValues = ContentValues(1).apply {
|
||||
put(PROFILE_AVATAR, profileAvatar)
|
||||
}
|
||||
@@ -1869,6 +1869,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
rotateStorageId(id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
} else if (forceNotify) {
|
||||
AppDependencies.databaseObserver.notifyRecipientChanged(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3815,8 +3817,15 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
|
||||
}
|
||||
}
|
||||
|
||||
fun manuallyShowAvatar(recipientId: RecipientId) {
|
||||
updateExtras(recipientId) { b: RecipientExtras.Builder -> b.manuallyShownAvatar(true) }
|
||||
fun clearHasGroupsInCommon(recipientId: RecipientId) {
|
||||
if (update(recipientId, contentValuesOf(GROUPS_IN_COMMON to 0))) {
|
||||
Log.i(TAG, "Reset $recipientId to have no groups in common.")
|
||||
Recipient.live(recipientId).refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun manuallyUpdateShowAvatar(recipientId: RecipientId, showAvatar: Boolean) {
|
||||
updateExtras(recipientId) { b: RecipientExtras.Builder -> b.manuallyShownAvatar(showAvatar) }
|
||||
}
|
||||
|
||||
fun getCapabilities(id: RecipientId): RecipientRecord.Capabilities? {
|
||||
|
||||
@@ -555,13 +555,13 @@ class GroupsV2StateProcessor private constructor(
|
||||
AppDependencies.jobManager.add(AvatarGroupsV2DownloadJob(groupId, updatedGroupState.avatar))
|
||||
}
|
||||
|
||||
profileAndMessageHelper.setProfileSharing(groupStateDiff, updatedGroupState)
|
||||
profileAndMessageHelper.setProfileSharing(groupStateDiff, updatedGroupState, needsAvatarFetch)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal class ProfileAndMessageHelper(private val aci: ACI, private val masterKey: GroupMasterKey, private val groupId: GroupId.V2) {
|
||||
|
||||
fun setProfileSharing(groupStateDiff: GroupStateDiff, newLocalState: DecryptedGroup) {
|
||||
fun setProfileSharing(groupStateDiff: GroupStateDiff, newLocalState: DecryptedGroup, needsAvatarFetch: Boolean) {
|
||||
val previousGroupState = groupStateDiff.previousGroupState
|
||||
|
||||
if (previousGroupState != null && DecryptedGroupUtil.findMemberByAci(previousGroupState.members, aci).isPresent) {
|
||||
@@ -591,8 +591,11 @@ class GroupsV2StateProcessor private constructor(
|
||||
return
|
||||
} else if ((addedBy.isSystemContact || addedBy.isProfileSharing) && !addedBy.isHidden) {
|
||||
Log.i(TAG, "Group 'adder' is trusted. contact: " + addedBy.isSystemContact + ", profileSharing: " + addedBy.isProfileSharing)
|
||||
Log.i(TAG, "Added to a group and auto-enabling profile sharing")
|
||||
Log.i(TAG, "Added to a group and auto-enabling profile sharing and redownloading avatar if needed")
|
||||
SignalDatabase.recipients.setProfileSharing(Recipient.externalGroupExact(groupId).id, true)
|
||||
if (needsAvatarFetch) {
|
||||
AppDependencies.jobManager.add(AvatarGroupsV2DownloadJob(groupId, newLocalState.avatar, true))
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "Added to a group, but not enabling profile sharing, as 'adder' is not trusted")
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
@@ -18,6 +20,7 @@ import org.thoughtcrime.securesms.jobmanager.JsonJobData;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ByteUnit;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||
@@ -38,32 +41,48 @@ public final class AvatarGroupsV2DownloadJob extends BaseJob {
|
||||
private static final long AVATAR_DOWNLOAD_FAIL_SAFE_MAX_SIZE = ByteUnit.MEGABYTES.toBytes(5);
|
||||
|
||||
private static final String KEY_GROUP_ID = "group_id";
|
||||
private static final String CDN_KEY = "cdn_key";
|
||||
private static final String KEY_CDN_KEY = "cdn_key";
|
||||
private static final String KEY_FORCE = "force";
|
||||
|
||||
private final GroupId.V2 groupId;
|
||||
private final String cdnKey;
|
||||
private final boolean force;
|
||||
|
||||
public static void enqueueUnblurredAvatar(@NonNull GroupId.V2 groupId) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
String cdnKey = SignalDatabase.groups().getGroup(groupId).get().requireV2GroupProperties().getAvatarKey();
|
||||
AppDependencies.getJobManager().add(new AvatarGroupsV2DownloadJob(groupId, cdnKey, true));
|
||||
});
|
||||
}
|
||||
|
||||
public AvatarGroupsV2DownloadJob(@NonNull GroupId.V2 groupId, @NonNull String cdnKey) {
|
||||
this(groupId, cdnKey, false);
|
||||
}
|
||||
|
||||
public AvatarGroupsV2DownloadJob(@NonNull GroupId.V2 groupId, @NonNull String cdnKey, boolean force) {
|
||||
this(new Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setQueue("AvatarGroupsV2DownloadJob::" + groupId)
|
||||
.setMaxAttempts(10)
|
||||
.build(),
|
||||
groupId,
|
||||
cdnKey);
|
||||
cdnKey,
|
||||
force);
|
||||
}
|
||||
|
||||
private AvatarGroupsV2DownloadJob(@NonNull Parameters parameters, @NonNull GroupId.V2 groupId, @NonNull String cdnKey) {
|
||||
private AvatarGroupsV2DownloadJob(@NonNull Parameters parameters, @NonNull GroupId.V2 groupId, @NonNull String cdnKey, boolean force) {
|
||||
super(parameters);
|
||||
this.groupId = groupId;
|
||||
this.cdnKey = cdnKey;
|
||||
this.force = force;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable byte[] serialize() {
|
||||
return new JsonJobData.Builder()
|
||||
.putString(KEY_GROUP_ID, groupId.toString())
|
||||
.putString(CDN_KEY, cdnKey)
|
||||
.putString(KEY_CDN_KEY, cdnKey)
|
||||
.putBoolean(KEY_FORCE, force)
|
||||
.serialize();
|
||||
}
|
||||
|
||||
@@ -91,13 +110,23 @@ public final class AvatarGroupsV2DownloadJob extends BaseJob {
|
||||
return;
|
||||
}
|
||||
|
||||
Recipient recipient = Recipient.resolved(record.get().getRecipientId());
|
||||
if (recipient.getShouldBlurAvatar() && !force) {
|
||||
Log.w(TAG, "Marking group as having an avatar but not downloading because avatar is blurred");
|
||||
database.onAvatarUpdated(groupId, true);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Downloading new avatar for group " + groupId);
|
||||
if (force) AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.IN_PROGRESS);
|
||||
byte[] decryptedAvatar = downloadGroupAvatarBytes(context, record.get().requireV2GroupProperties().getGroupMasterKey(), cdnKey);
|
||||
|
||||
AvatarHelper.setAvatar(context, record.get().getRecipientId(), decryptedAvatar != null ? new ByteArrayInputStream(decryptedAvatar) : null);
|
||||
database.onAvatarUpdated(groupId, true);
|
||||
if (force) AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.FINISHED);
|
||||
|
||||
} catch (NonSuccessfulResponseCodeException e) {
|
||||
if (force) AvatarDownloadStateCache.set(Recipient.resolved(record.get().getRecipientId()), AvatarDownloadStateCache.DownloadState.FAILED);
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
@@ -136,7 +165,9 @@ public final class AvatarGroupsV2DownloadJob extends BaseJob {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {}
|
||||
public void onFailure() {
|
||||
if (force) AvatarDownloadStateCache.set(Recipient.externalPossiblyMigratedGroup(groupId), AvatarDownloadStateCache.DownloadState.FAILED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShouldRetry(@NonNull Exception exception) {
|
||||
@@ -150,7 +181,8 @@ public final class AvatarGroupsV2DownloadJob extends BaseJob {
|
||||
|
||||
return new AvatarGroupsV2DownloadJob(parameters,
|
||||
GroupId.parseOrThrow(data.getString(KEY_GROUP_ID)).requireV2(),
|
||||
data.getString(CDN_KEY));
|
||||
data.getString(KEY_CDN_KEY),
|
||||
data.getBooleanOrDefault(KEY_FORCE, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.annotation.Nullable;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.RecipientTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
@@ -41,35 +42,47 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
||||
private static final String KEY_PROFILE_AVATAR = "profile_avatar";
|
||||
private static final String KEY_RECIPIENT = "recipient";
|
||||
private static final String KEY_FORCE_UPDATE = "force";
|
||||
private static final String KEY_FOR_UNBLURRED = "for_unblurred";
|
||||
|
||||
private final String profileAvatar;
|
||||
private final Recipient recipient;
|
||||
private final boolean forceUpdate;
|
||||
private final boolean forUnblurred;
|
||||
|
||||
public static void enqueueForceUpdate(Recipient recipient) {
|
||||
SignalExecutors.BOUNDED.execute(() -> AppDependencies.getJobManager().add(new RetrieveProfileAvatarJob(recipient, recipient.resolve().getProfileAvatar(), true)));
|
||||
}
|
||||
|
||||
public static void enqueueUnblurredAvatar(Recipient recipient) {
|
||||
SignalExecutors.BOUNDED.execute(() -> AppDependencies.getJobManager().add(new RetrieveProfileAvatarJob(recipient, recipient.resolve().getProfileAvatar(), true, true)));
|
||||
}
|
||||
|
||||
public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar) {
|
||||
this(recipient, profileAvatar, false);
|
||||
}
|
||||
|
||||
public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar, boolean forceUpdate) {
|
||||
this(recipient, profileAvatar, forceUpdate, false);
|
||||
}
|
||||
|
||||
public RetrieveProfileAvatarJob(Recipient recipient, String profileAvatar, boolean forceUpdate, boolean forUnblurred) {
|
||||
this(new Job.Parameters.Builder().setQueue("RetrieveProfileAvatarJob::" + recipient.getId().toQueueKey())
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.HOURS.toMillis(1))
|
||||
.build(),
|
||||
recipient,
|
||||
profileAvatar,
|
||||
forceUpdate);
|
||||
forceUpdate,
|
||||
forUnblurred);
|
||||
}
|
||||
|
||||
private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar, boolean forceUpdate) {
|
||||
private RetrieveProfileAvatarJob(@NonNull Job.Parameters parameters, @NonNull Recipient recipient, String profileAvatar, boolean forceUpdate, boolean forUnblurred) {
|
||||
super(parameters);
|
||||
|
||||
this.recipient = recipient;
|
||||
this.profileAvatar = profileAvatar;
|
||||
this.forceUpdate = forceUpdate;
|
||||
this.forUnblurred = forUnblurred;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -77,6 +90,7 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
||||
return new JsonJobData.Builder().putString(KEY_PROFILE_AVATAR, profileAvatar)
|
||||
.putString(KEY_RECIPIENT, recipient.getId().serialize())
|
||||
.putBoolean(KEY_FORCE_UPDATE, forceUpdate)
|
||||
.putBoolean(KEY_FOR_UNBLURRED, forUnblurred)
|
||||
.serialize();
|
||||
}
|
||||
|
||||
@@ -95,10 +109,17 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
||||
return;
|
||||
}
|
||||
|
||||
if (recipient.getShouldBlurAvatar() && !forUnblurred) {
|
||||
Log.w(TAG, "Saving profile avatar but not downloading the file because avatar is blurred");
|
||||
database.setProfileAvatar(recipient.getId(), profileAvatar, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (forUnblurred) AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.IN_PROGRESS);
|
||||
ProfileAvatarFileDetails details = recipient.getProfileAvatarFileDetails();
|
||||
if (!details.hasFile() || forceUpdate) {
|
||||
if (details.getLastModified() > System.currentTimeMillis() || details.getLastModified() + MIN_TIME_BETWEEN_FORCE_RETRY < System.currentTimeMillis()) {
|
||||
Log.i(TAG, "Forcing re-download of avatar. hasFile: " + details.hasFile());
|
||||
if (!details.hasFile() || forceUpdate || forUnblurred) {
|
||||
if (details.getLastModified() > System.currentTimeMillis() || details.getLastModified() + MIN_TIME_BETWEEN_FORCE_RETRY < System.currentTimeMillis() || forUnblurred) {
|
||||
Log.i(TAG, "Forcing re-download of avatar. hasFile: " + details.hasFile() + " unblurred: " + forUnblurred);
|
||||
} else {
|
||||
Log.i(TAG, "Too early to force re-download avatar. hasFile: " + details.hasFile());
|
||||
return;
|
||||
@@ -112,8 +133,9 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
||||
if (AvatarHelper.hasAvatar(context, recipient.getId())) {
|
||||
Log.w(TAG, "Removing profile avatar (no url) for: " + recipient.getId().serialize());
|
||||
AvatarHelper.delete(context, recipient.getId());
|
||||
database.setProfileAvatar(recipient.getId(), profileAvatar);
|
||||
database.setProfileAvatar(recipient.getId(), profileAvatar, false);
|
||||
}
|
||||
if (forUnblurred) AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.FAILED);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -129,10 +151,12 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
||||
if (recipient.isSelf()) {
|
||||
SignalStore.misc().setHasEverHadAnAvatar(true);
|
||||
}
|
||||
if (forUnblurred) AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.FINISHED);
|
||||
} catch (AssertionError e) {
|
||||
throw new IOException("Failed to copy stream. Likely a Conscrypt issue.", e);
|
||||
}
|
||||
} catch (PushNetworkException e) {
|
||||
if (forUnblurred) AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.FAILED);
|
||||
if (e.getCause() instanceof NonSuccessfulResponseCodeException) {
|
||||
Log.w(TAG, "Removing profile avatar (no image available) for: " + recipient.getId().serialize());
|
||||
AvatarHelper.delete(context, recipient.getId());
|
||||
@@ -143,7 +167,7 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
||||
if (downloadDestination != null) downloadDestination.delete();
|
||||
}
|
||||
|
||||
database.setProfileAvatar(recipient.getId(), profileAvatar);
|
||||
database.setProfileAvatar(recipient.getId(), profileAvatar, forUnblurred);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -154,6 +178,7 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
if (forUnblurred) AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.FAILED);
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<RetrieveProfileAvatarJob> {
|
||||
@@ -165,7 +190,8 @@ public class RetrieveProfileAvatarJob extends BaseJob {
|
||||
return new RetrieveProfileAvatarJob(parameters,
|
||||
Recipient.resolved(RecipientId.from(data.getString(KEY_RECIPIENT))),
|
||||
data.getString(KEY_PROFILE_AVATAR),
|
||||
data.getBooleanOrDefault(KEY_FORCE_UPDATE, false));
|
||||
data.getBooleanOrDefault(KEY_FORCE_UPDATE, false),
|
||||
data.getBooleanOrDefault(KEY_FOR_UNBLURRED, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@ public final class MessageRequestRepository {
|
||||
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
|
||||
if (sharedGroups.isEmpty() && recipient.getHasGroupsInCommon()) {
|
||||
SignalDatabase.recipients().clearHasGroupsInCommon(recipient.getId());
|
||||
}
|
||||
|
||||
return new MessageRequestRecipientInfo(
|
||||
recipient,
|
||||
groupInfo,
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.MultiTransformation
|
||||
import com.bumptech.glide.load.Transformation
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.CircleCrop
|
||||
import org.thoughtcrime.securesms.R
|
||||
@@ -17,12 +16,11 @@ import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar
|
||||
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarGradientColors
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
|
||||
import org.thoughtcrime.securesms.notifications.NotificationIds
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil
|
||||
import org.thoughtcrime.securesms.util.BlurTransformation
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
fun Drawable?.toLargeBitmap(context: Context): Bitmap? {
|
||||
@@ -38,18 +36,14 @@ fun Drawable?.toLargeBitmap(context: Context): Bitmap? {
|
||||
fun Recipient.getContactDrawable(context: Context): Drawable? {
|
||||
val contactPhoto: ContactPhoto? = if (isSelf) ProfileContactPhoto(this) else contactPhoto
|
||||
val fallbackAvatar: FallbackAvatar = if (isSelf) getFallback(context) else getFallbackAvatar()
|
||||
return if (contactPhoto != null) {
|
||||
return if (shouldBlurAvatar && hasAvatar) {
|
||||
return AvatarGradientColors.getGradientDrawable(this)
|
||||
} else if (contactPhoto != null) {
|
||||
try {
|
||||
val transforms: MutableList<Transformation<Bitmap>> = mutableListOf()
|
||||
if (shouldBlurAvatar) {
|
||||
transforms += BlurTransformation(AppDependencies.application, 0.25f, BlurTransformation.MAX_RADIUS)
|
||||
}
|
||||
transforms += CircleCrop()
|
||||
|
||||
Glide.with(context.applicationContext)
|
||||
.load(contactPhoto)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.transform(MultiTransformation(transforms))
|
||||
.transform(MultiTransformation(listOf(CircleCrop())))
|
||||
.submit(
|
||||
context.resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
|
||||
context.resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar;
|
||||
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarGradientColors;
|
||||
import org.thoughtcrime.securesms.providers.AvatarProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
@@ -132,10 +133,14 @@ public final class AvatarUtil {
|
||||
AvatarTarget avatarTarget = new AvatarTarget(size);
|
||||
RequestManager requestManager = Glide.with(context);
|
||||
|
||||
requestCircle(requestManager.asBitmap(), context, recipient, size).into(avatarTarget);
|
||||
if (recipient.getShouldBlurAvatar() && recipient.getHasAvatar()) {
|
||||
return DrawableUtil.toBitmap(AvatarGradientColors.getGradientDrawable(recipient), size, size);
|
||||
} else {
|
||||
requestCircle(requestManager.asBitmap(), context, recipient, size).into(avatarTarget);
|
||||
|
||||
Bitmap bitmap = avatarTarget.await();
|
||||
return Objects.requireNonNullElseGet(bitmap, () -> DrawableUtil.toBitmap(getFallback(context, recipient, size), size, size));
|
||||
Bitmap bitmap = avatarTarget.await();
|
||||
return Objects.requireNonNullElseGet(bitmap, () -> DrawableUtil.toBitmap(getFallback(context, recipient, size), size, size));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
return DrawableUtil.toBitmap(getFallback(context, recipient, size), size, size);
|
||||
}
|
||||
@@ -168,14 +173,7 @@ public final class AvatarUtil {
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.override(size);
|
||||
|
||||
if (recipient.getShouldBlurAvatar()) {
|
||||
BlurTransformation blur = new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS);
|
||||
if (transformation != null) {
|
||||
return request.transform(blur, transformation);
|
||||
} else {
|
||||
return request.transform(blur);
|
||||
}
|
||||
} else if (transformation != null) {
|
||||
if (transformation != null) {
|
||||
return request.transform(transformation);
|
||||
} else {
|
||||
return request;
|
||||
|
||||
@@ -378,7 +378,7 @@ public final class ProfileUtil {
|
||||
SignalStore.phoneNumberPrivacy().isPhoneNumberSharingEnabled()).orElse(null);
|
||||
SignalStore.registration().setHasUploadedProfile(true);
|
||||
if (!avatar.keepTheSame) {
|
||||
SignalDatabase.recipients().setProfileAvatar(Recipient.self().getId(), avatarPath);
|
||||
SignalDatabase.recipients().setProfileAvatar(Recipient.self().getId(), avatarPath, false);
|
||||
}
|
||||
AppDependencies.getJobManager().add(new RefreshOwnProfileJob());
|
||||
}
|
||||
|
||||
13
app/src/main/res/drawable/circle_profile_photo.xml
Normal file
13
app/src/main/res/drawable/circle_profile_photo.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="80dp"
|
||||
android:height="80dp"
|
||||
android:viewportWidth="80"
|
||||
android:viewportHeight="80">
|
||||
<path
|
||||
android:pathData="M40,80C62.091,80 80,62.091 80,40C80,17.909 62.091,0 40,0C17.909,0 0,17.909 0,40C0,62.091 17.909,80 40,80Z"
|
||||
android:fillColor="#FBFCFE"/>
|
||||
<path
|
||||
android:pathData="M40,80C62.091,80 80,62.091 80,40C80,17.909 62.091,0 40,0C17.909,0 0,17.909 0,40C0,62.091 17.909,80 40,80Z"
|
||||
android:fillColor="#50679F"
|
||||
android:fillAlpha="0.11"/>
|
||||
</vector>
|
||||
@@ -16,6 +16,21 @@
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
android:indeterminate="true"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:indeterminateTint="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintTop_toTopOf="@id/message_request_avatar"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_request_avatar"
|
||||
app:layout_constraintStart_toStartOf="@id/message_request_avatar"
|
||||
app:layout_constraintEnd_toEndOf="@id/message_request_avatar"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
android:id="@+id/message_request_badge"
|
||||
android:layout_width="36dp"
|
||||
@@ -31,8 +46,6 @@
|
||||
android:id="@+id/message_request_avatar_tap_to_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/circle_tint_darker"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
|
||||
@@ -56,6 +56,49 @@
|
||||
app:layout_constraintTop_toTopOf="@+id/rbs_recipient_avatar"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/rbs_progress_bar"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
android:indeterminate="true"
|
||||
style="?android:attr/progressBarStyleLarge"
|
||||
android:indeterminateTint="@color/signal_colorOnSurfaceVariant"
|
||||
app:layout_constraintTop_toTopOf="@id/rbs_recipient_avatar"
|
||||
app:layout_constraintBottom_toBottomOf="@id/rbs_recipient_avatar"
|
||||
app:layout_constraintStart_toStartOf="@id/rbs_recipient_avatar"
|
||||
app:layout_constraintEnd_toEndOf="@id/rbs_recipient_avatar"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/rbs_tap_to_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/rbs_recipient_avatar"
|
||||
app:layout_constraintEnd_toEndOf="@+id/rbs_recipient_avatar"
|
||||
app:layout_constraintStart_toStartOf="@+id/rbs_recipient_avatar"
|
||||
app:layout_constraintTop_toTopOf="@+id/rbs_recipient_avatar">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
app:srcCompat="@drawable/ic_tap_outline_24"
|
||||
app:tint="@color/core_white" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/MessageRequestProfileView_view"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Subtitle"
|
||||
android:textColor="@color/core_white" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/rbs_full_name"
|
||||
android:layout_width="0dp"
|
||||
|
||||
@@ -680,6 +680,8 @@
|
||||
<string name="ConversationFragment_profile_names">Profile names</string>
|
||||
<!-- Placeholder text shown in conversation header that when clicked will open a dialog about group names -->
|
||||
<string name="ConversationFragment_group_names">Group names</string>
|
||||
<!-- Snackbar toast message shown when a profile cannot be downloaded and to try again. -->
|
||||
<string name="ConversationFragment_photo_failed">Photo failed to download. Try again.</string>
|
||||
|
||||
<!-- Title of Safety Tips bottom sheet dialog -->
|
||||
<string name="SafetyTips_title">Safety Tips</string>
|
||||
|
||||
Reference in New Issue
Block a user