Update chat colors.
This commit is contained in:
parent
36fe150678
commit
bcc5d485ab
164 changed files with 5817 additions and 1476 deletions
|
@ -322,6 +322,7 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||
lintChecks project(':lintchecks')
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
|
|
@ -11,6 +11,8 @@ import androidx.lifecycle.Observer;
|
|||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
|
@ -29,7 +31,7 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable {
|
||||
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable {
|
||||
void bind(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull ConversationMessage messageRecord,
|
||||
@NonNull Optional<MessageRecord> previousMessageRecord,
|
||||
|
@ -43,7 +45,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable {
|
|||
boolean hasWallpaper,
|
||||
boolean isMessageRequestAccepted,
|
||||
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||
boolean canPlayInline);
|
||||
boolean canPlayInline,
|
||||
@NonNull Colorizer colorizer);
|
||||
|
||||
ConversationMessage getConversationMessage();
|
||||
|
||||
|
|
|
@ -218,12 +218,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
|
|||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private void setActionBarNotificationBarColor(MaterialColor color) {
|
||||
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
|
||||
|
||||
WindowUtil.setStatusBarColor(getWindow(), color.toStatusBarColor(this));
|
||||
}
|
||||
|
||||
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
|
||||
|
||||
public static final String RECIPIENT_ID = "recipient_id";
|
||||
|
|
|
@ -91,14 +91,14 @@ public enum MaterialColor {
|
|||
}
|
||||
|
||||
public @ColorRes int toQuoteBarColorResource(@NonNull Context context, boolean outgoing) {
|
||||
if (outgoing) {
|
||||
if (!outgoing) {
|
||||
return isDarkTheme(context) ? tintColor : shadeColor ;
|
||||
}
|
||||
return R.color.core_white;
|
||||
}
|
||||
|
||||
public @ColorInt int toQuoteBackgroundColor(@NonNull Context context, boolean outgoing) {
|
||||
if (outgoing) {
|
||||
if (!outgoing) {
|
||||
int color = toConversationColor(context);
|
||||
int alpha = isDarkTheme(context) ? (int) (0.2 * 255) : (int) (0.4 * 255);
|
||||
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
|
||||
|
@ -108,7 +108,7 @@ public enum MaterialColor {
|
|||
}
|
||||
|
||||
public @ColorInt int toQuoteFooterColor(@NonNull Context context, boolean outgoing) {
|
||||
if (outgoing) {
|
||||
if (!outgoing) {
|
||||
int color = toConversationColor(context);
|
||||
int alpha = isDarkTheme(context) ? (int) (0.4 * 255) : (int) (0.6 * 255);
|
||||
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
|
||||
|
|
|
@ -28,6 +28,8 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
|||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
|
@ -73,6 +75,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||
private OnClickListener listener;
|
||||
private Recipient.FallbackPhotoProvider fallbackPhotoProvider;
|
||||
private boolean blurred;
|
||||
private ChatColors chatColors;
|
||||
|
||||
private @Nullable RecipientContactPhoto recipientContactPhoto;
|
||||
private @NonNull Drawable unknownRecipientDrawable;
|
||||
|
@ -99,8 +102,9 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||
|
||||
outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
|
||||
|
||||
unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted);
|
||||
unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20).asDrawable(getContext(), ChatColorsPalette.UNKNOWN_CONTACT, inverted);
|
||||
blurred = false;
|
||||
chatColors = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -171,10 +175,12 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||
Recipient.self().getProfileAvatar()))
|
||||
: new RecipientContactPhoto(recipient);
|
||||
|
||||
boolean shouldBlur = recipient.shouldBlurAvatar();
|
||||
boolean shouldBlur = recipient.shouldBlurAvatar();
|
||||
ChatColors chatColors = recipient.getChatColors();
|
||||
|
||||
if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred) {
|
||||
if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred || !Objects.equals(chatColors, this.chatColors)) {
|
||||
requestManager.clear(this);
|
||||
this.chatColors = chatColors;
|
||||
recipientContactPhoto = photo;
|
||||
|
||||
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
|
||||
|
@ -207,7 +213,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||
requestManager.clear(this);
|
||||
if (fallbackPhotoProvider != null) {
|
||||
setImageDrawable(fallbackPhotoProvider.getPhotoForRecipientWithoutName()
|
||||
.asDrawable(getContext(), MaterialColor.STEEL.toAvatarColor(getContext()), inverted));
|
||||
.asDrawable(getContext(), ChatColorsPalette.Bubbles.STEEL, inverted));
|
||||
} else {
|
||||
setImageDrawable(unknownRecipientDrawable);
|
||||
}
|
||||
|
@ -240,11 +246,11 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||
|
||||
public void setImageBytesForGroup(@Nullable byte[] avatarBytes,
|
||||
@Nullable Recipient.FallbackPhotoProvider fallbackPhotoProvider,
|
||||
@NonNull MaterialColor color)
|
||||
@NonNull ChatColors color)
|
||||
{
|
||||
Drawable fallback = Util.firstNonNull(fallbackPhotoProvider, Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER)
|
||||
.getPhotoForGroup()
|
||||
.asDrawable(getContext(), color.toAvatarColor(getContext()));
|
||||
.asDrawable(getContext(), color);
|
||||
|
||||
GlideApp.with(this)
|
||||
.load(avatarBytes)
|
||||
|
@ -285,7 +291,7 @@ public final class AvatarImageView extends AppCompatImageView {
|
|||
if (other == null) return false;
|
||||
|
||||
return other.recipient.equals(recipient) &&
|
||||
other.recipient.getColor().equals(recipient.getColor()) &&
|
||||
other.recipient.getChatColors().equals(recipient.getChatColors()) &&
|
||||
other.ready == ready &&
|
||||
Objects.equals(other.contactPhoto, contactPhoto);
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
|||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
|
||||
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
|
||||
|
@ -146,6 +147,14 @@ public class ConversationItemFooter extends LinearLayout {
|
|||
setBackground(null);
|
||||
}
|
||||
|
||||
public @Nullable Projection getProjection() {
|
||||
if (getVisibility() == VISIBLE) {
|
||||
return Projection.relativeToViewRoot(this, new Projection.Corners(ViewUtil.dpToPx(11)));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
|
||||
dateView.forceLayout();
|
||||
if (messageRecord.isFailed()) {
|
||||
|
|
|
@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
|||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -116,8 +117,8 @@ public class ConversationItemThumbnail extends FrameLayout {
|
|||
thumbnail.setAlpha(1f);
|
||||
}
|
||||
|
||||
public @Nullable CornerMask getCornerMask() {
|
||||
return cornerMask;
|
||||
public @NonNull Projection.Corners getCorners() {
|
||||
return new Projection.Corners(cornerMask.getRadii());
|
||||
}
|
||||
|
||||
public void setPulseOutliner(@NonNull Outliner outliner) {
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
@ -41,7 +43,6 @@ public class ConversationTypingView extends LinearLayout {
|
|||
}
|
||||
|
||||
Recipient typist = typists.get(0);
|
||||
bubble.getBackground().setColorFilter(typist.getColor().toConversationColor(getContext()), PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
if (isGroupThread) {
|
||||
avatar.setAvatar(glideRequests, typist, true);
|
||||
|
|
|
@ -22,20 +22,12 @@ public class CornerMask {
|
|||
private final RectF bounds = new RectF();
|
||||
|
||||
public CornerMask(@NonNull View view) {
|
||||
this(view, null);
|
||||
}
|
||||
|
||||
public CornerMask(@NonNull View view, @Nullable CornerMask toClone) {
|
||||
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
||||
|
||||
clearPaint.setColor(Color.BLACK);
|
||||
clearPaint.setStyle(Paint.Style.FILL);
|
||||
clearPaint.setAntiAlias(true);
|
||||
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
|
||||
if (toClone != null) {
|
||||
System.arraycopy(toClone.radii, 0, radii, 0, radii.length);
|
||||
}
|
||||
}
|
||||
|
||||
public void mask(Canvas canvas) {
|
||||
|
@ -64,6 +56,13 @@ public class CornerMask {
|
|||
radii[6] = radii[7] = bottomLeft;
|
||||
}
|
||||
|
||||
public void setRadii(float topLeft, float topRight, float bottomRight, float bottomLeft) {
|
||||
radii[0] = radii[1] = topLeft;
|
||||
radii[2] = radii[3] = topRight;
|
||||
radii[4] = radii[5] = bottomRight;
|
||||
radii[6] = radii[7] = bottomLeft;
|
||||
}
|
||||
|
||||
public void setTopLeftRadius(int radius) {
|
||||
radii[0] = radii[1] = radius;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
|
|||
import android.animation.Animator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.AttributeSet;
|
||||
|
@ -22,6 +23,7 @@ import androidx.annotation.DimenRes;
|
|||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
|
@ -33,6 +35,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
|||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdapter;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
|
@ -42,7 +45,6 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
|||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
|
@ -50,6 +52,7 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
|||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class InputPanel extends LinearLayout
|
||||
|
@ -74,7 +77,7 @@ public class InputPanel extends LinearLayout
|
|||
private View buttonToggle;
|
||||
private View recordingContainer;
|
||||
private View recordLockCancel;
|
||||
private View composeContainer;
|
||||
private ViewGroup composeContainer;
|
||||
|
||||
private MicrophoneRecorderView microphoneRecorderView;
|
||||
private SlideToCancel slideToCancel;
|
||||
|
@ -161,9 +164,10 @@ public class InputPanel extends LinearLayout
|
|||
long id,
|
||||
@NonNull Recipient author,
|
||||
@NonNull CharSequence body,
|
||||
@NonNull SlideDeck attachments)
|
||||
@NonNull SlideDeck attachments,
|
||||
@NonNull Colorizer colorizer)
|
||||
{
|
||||
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments);
|
||||
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments, colorizer);
|
||||
|
||||
int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight()
|
||||
: 0;
|
||||
|
@ -290,11 +294,11 @@ public class InputPanel extends LinearLayout
|
|||
|
||||
public void setWallpaperEnabled(boolean enabled) {
|
||||
if (enabled) {
|
||||
setBackgroundColor(getContext().getResources().getColor(R.color.wallpaper_compose_background));
|
||||
composeContainer.setBackgroundResource(R.drawable.compose_background_wallpaper);
|
||||
setBackground(new ColorDrawable(getContext().getResources().getColor(R.color.wallpaper_compose_background)));
|
||||
composeContainer.setBackground(Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.compose_background_wallpaper)));
|
||||
} else {
|
||||
setBackgroundColor(getResources().getColor(R.color.signal_background_primary));
|
||||
composeContainer.setBackgroundResource(R.drawable.compose_background);
|
||||
setBackground(new ColorDrawable(getContext().getResources().getColor(R.color.signal_background_primary)));
|
||||
composeContainer.setBackground(Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.compose_background)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import android.widget.TextView;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
@ -25,6 +26,7 @@ import org.signal.core.util.logging.Log;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
@ -33,6 +35,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
|
|||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -49,7 +52,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
private ViewGroup footerView;
|
||||
private TextView authorView;
|
||||
private TextView bodyView;
|
||||
private ImageView quoteBarView;
|
||||
private View quoteBarView;
|
||||
private ImageView thumbnailView;
|
||||
private View attachmentVideoOverlayView;
|
||||
private ViewGroup attachmentContainerView;
|
||||
|
@ -66,6 +69,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
private int largeCornerRadius;
|
||||
private int smallCornerRadius;
|
||||
private CornerMask cornerMask;
|
||||
private Colorizer colorizer;
|
||||
|
||||
|
||||
public QuoteView(Context context) {
|
||||
|
@ -152,7 +156,8 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
@NonNull Recipient author,
|
||||
@Nullable CharSequence body,
|
||||
boolean originalMissing,
|
||||
@NonNull SlideDeck attachments)
|
||||
@NonNull SlideDeck attachments,
|
||||
@NonNull Colorizer colorizer)
|
||||
{
|
||||
if (this.author != null) this.author.removeForeverObserver(this);
|
||||
|
||||
|
@ -160,6 +165,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
this.author = author.live();
|
||||
this.body = body;
|
||||
this.attachments = attachments;
|
||||
this.colorizer = colorizer;
|
||||
|
||||
this.author.observeForever(this);
|
||||
setQuoteAuthor(author);
|
||||
|
@ -188,15 +194,22 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
setQuoteAuthor(recipient);
|
||||
}
|
||||
|
||||
public @NonNull Projection getProjection(@NonNull ViewGroup parent) {
|
||||
return Projection.relativeToParent(parent, this, getCorners());
|
||||
}
|
||||
|
||||
public @NonNull Projection.Corners getCorners() {
|
||||
return new Projection.Corners(cornerMask.getRadii());
|
||||
}
|
||||
|
||||
private void setQuoteAuthor(@NonNull Recipient author) {
|
||||
boolean outgoing = messageType != MESSAGE_TYPE_INCOMING;
|
||||
boolean outgoing = messageType == MESSAGE_TYPE_OUTGOING;
|
||||
|
||||
authorView.setText(author.isSelf() ? getContext().getString(R.string.QuoteView_you)
|
||||
: author.getDisplayName(getContext()));
|
||||
|
||||
// We use the raw color resource because Android 4.x was struggling with tints here
|
||||
quoteBarView.setImageResource(author.getColor().toQuoteBarColorResource(getContext(), outgoing));
|
||||
mainView.setBackgroundColor(author.getColor().toQuoteBackgroundColor(getContext(), outgoing));
|
||||
quoteBarView.setBackgroundColor(ContextCompat.getColor(getContext(), outgoing ? R.color.core_white : android.R.color.transparent));
|
||||
mainView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.quote_view_background));
|
||||
}
|
||||
|
||||
private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments) {
|
||||
|
@ -272,7 +285,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
|
||||
private void setQuoteMissingFooter(boolean missing) {
|
||||
footerView.setVisibility(missing ? VISIBLE : GONE);
|
||||
footerView.setBackgroundColor(author.get().getColor().toQuoteFooterColor(getContext(), messageType != MESSAGE_TYPE_INCOMING));
|
||||
footerView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.quote_view_background));
|
||||
}
|
||||
|
||||
public long getQuoteId() {
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import kotlin.jvm.functions.Function1;
|
||||
import kotlin.jvm.functions.Function2;
|
||||
|
||||
/**
|
||||
* Drawable which renders a gradient at a specified angle. Note that this drawable does
|
||||
* not implement drawable state, and all the baggage that comes with a normal Drawable
|
||||
* override, so this may not work in every scenario.
|
||||
*
|
||||
* Essentially, this drawable creates a LinearGradient shader using the given colors and
|
||||
* positions, but makes it larger than the bounds, such that it can be rotated and still
|
||||
* fill the bounds with a gradient.
|
||||
*
|
||||
* If you wish to apply clipping to this drawable, it is recommended to either use it with
|
||||
* a CardView or utilize {@link org.thoughtcrime.securesms.util.CustomDrawWrapperKt#customizeOnDraw(Drawable, Function2)}
|
||||
*/
|
||||
public final class RotatableGradientDrawable extends Drawable {
|
||||
|
||||
/**
|
||||
* From investigation into how Gradients are rendered vs how they are rendered in
|
||||
* designs, in order to match spec, we need to rotate gradients by 225 degrees. (180 + 45)
|
||||
*
|
||||
* This puts 0 at the bottom (0, -1) of the surface area.
|
||||
*/
|
||||
private static final float DEGREE_OFFSET = 225f;
|
||||
|
||||
private final float degrees;
|
||||
private final int[] colors;
|
||||
private final float[] positions;
|
||||
|
||||
private final Rect fillRect = new Rect();
|
||||
private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
|
||||
|
||||
/**
|
||||
* @param degrees Gradient rotation in degrees, relative to a vector pointed from the center to bottom center
|
||||
* @param colors The colors of the gradient
|
||||
* @param positions The positions of the colors. Values should be between 0f and 1f and this array should be the
|
||||
* same length as colors.
|
||||
*/
|
||||
public RotatableGradientDrawable(float degrees, int[] colors, @Nullable float[] positions) {
|
||||
this.degrees = degrees + DEGREE_OFFSET;
|
||||
this.colors = colors;
|
||||
this.positions = positions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBounds(int left, int top, int right, int bottom) {
|
||||
super.setBounds(left, top, right, bottom);
|
||||
|
||||
Point topLeft = new Point(left, top);
|
||||
Point topRight = new Point(right, top);
|
||||
Point bottomLeft = new Point(left, bottom);
|
||||
Point bottomRight = new Point(right, bottom);
|
||||
Point origin = new Point(getBounds().width() / 2, getBounds().height() / 2);
|
||||
|
||||
Point rotationTopLeft = cornerPrime(origin, topLeft, degrees);
|
||||
Point rotationTopRight = cornerPrime(origin, topRight, degrees);
|
||||
Point rotationBottomLeft = cornerPrime(origin, bottomLeft, degrees);
|
||||
Point rotationBottomRight = cornerPrime(origin, bottomRight, degrees);
|
||||
|
||||
fillRect.left = Integer.MAX_VALUE;
|
||||
fillRect.top = Integer.MAX_VALUE;
|
||||
fillRect.right = Integer.MIN_VALUE;
|
||||
fillRect.bottom = Integer.MIN_VALUE;
|
||||
|
||||
for (Point point : Arrays.asList(topLeft, topRight, bottomLeft, bottomRight, rotationTopLeft, rotationTopRight, rotationBottomLeft, rotationBottomRight)) {
|
||||
if (point.x < fillRect.left) {
|
||||
fillRect.left = point.x;
|
||||
}
|
||||
|
||||
if (point.x > fillRect.right) {
|
||||
fillRect.right = point.x;
|
||||
}
|
||||
|
||||
if (point.y < fillRect.top) {
|
||||
fillRect.top = point.y;
|
||||
}
|
||||
|
||||
if (point.y > fillRect.bottom) {
|
||||
fillRect.bottom = point.y;
|
||||
}
|
||||
}
|
||||
|
||||
fillPaint.setShader(new LinearGradient(fillRect.left, fillRect.top, fillRect.right, fillRect.bottom, colors, positions, Shader.TileMode.CLAMP));
|
||||
}
|
||||
|
||||
private static Point cornerPrime(@NonNull Point origin, @NonNull Point corner, float degrees) {
|
||||
return new Point(xPrime(origin, corner, Math.toRadians(degrees)), yPrime(origin, corner, Math.toRadians(degrees)));
|
||||
}
|
||||
|
||||
private static int xPrime(@NonNull Point origin, @NonNull Point corner, double theta) {
|
||||
return (int) Math.ceil(((corner.x - origin.x) * Math.cos(theta)) - ((corner.y - origin.y) * Math.sin(theta)) + origin.x);
|
||||
}
|
||||
|
||||
private static int yPrime(@NonNull Point origin, @NonNull Point corner, double theta) {
|
||||
return (int) Math.ceil(((corner.x - origin.x) * Math.sin(theta)) + ((corner.y - origin.y) * Math.cos(theta)) + origin.y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
int save = canvas.save();
|
||||
canvas.rotate(degrees, getBounds().width() / 2f, getBounds().height() / 2f);
|
||||
canvas.drawRect(fillRect, fillPaint);
|
||||
canvas.restoreToCount(save);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
// Not supported
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
// Not supported
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.OPAQUE;
|
||||
}
|
||||
}
|
|
@ -42,7 +42,7 @@ class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__app
|
|||
)
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__chat_wallpaper),
|
||||
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
|
||||
onClick = {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.action_appearanceSettings_to_wallpaperActivity)
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ class VoiceNoteMediaDescriptionCompatFactory {
|
|||
extras.putString(EXTRA_AVATAR_RECIPIENT_ID, avatarRecipient.getId().serialize());
|
||||
extras.putLong(EXTRA_MESSAGE_POSITION, startingPosition);
|
||||
extras.putLong(EXTRA_THREAD_ID, messageRecord.getThreadId());
|
||||
extras.putString(EXTRA_COLOR, threadRecipient.getColor().serialize());
|
||||
extras.putLong(EXTRA_COLOR, threadRecipient.getChatColors().asSingleColor());
|
||||
extras.putLong(EXTRA_MESSAGE_ID, messageRecord.getId());
|
||||
|
||||
NotificationPrivacyPreference preference = SignalStore.settings().getMessageNotificationsPrivacy();
|
||||
|
|
|
@ -19,6 +19,8 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -99,14 +101,13 @@ class VoiceNoteNotificationManager {
|
|||
int startingPosition = (int) controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_MESSAGE_POSITION);
|
||||
long threadId = controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_THREAD_ID);
|
||||
|
||||
MaterialColor color;
|
||||
try {
|
||||
color = MaterialColor.fromSerialized(controller.getMetadata().getString(VoiceNoteMediaDescriptionCompatFactory.EXTRA_COLOR));
|
||||
} catch (MaterialColor.UnknownColorException e) {
|
||||
color = ContactColors.UNKNOWN_COLOR;
|
||||
int color = (int) controller.getMetadata().getLong(VoiceNoteMediaDescriptionCompatFactory.EXTRA_COLOR);
|
||||
|
||||
if (color == 0) {
|
||||
color = ChatColorsPalette.UNKNOWN_CONTACT.asSingleColor();
|
||||
}
|
||||
|
||||
notificationManager.setColor(color.toNotificationColor(context));
|
||||
notificationManager.setColor(color);
|
||||
|
||||
Intent conversationActivity = ConversationIntents.createBuilder(context, recipientId, threadId)
|
||||
.withStartingPosition(startingPosition)
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
|||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -225,7 +226,10 @@ public class CallParticipantView extends ConstraintLayout {
|
|||
.into(pipAvatar);
|
||||
|
||||
pipAvatar.setScaleType(contactPhoto == null ? ImageView.ScaleType.CENTER_INSIDE : ImageView.ScaleType.CENTER_CROP);
|
||||
pipAvatar.setBackgroundColor(recipient.getColor().toActionBarColor(getContext()));
|
||||
|
||||
ChatColors chatColors = recipient.getChatColors();
|
||||
|
||||
pipAvatar.setBackground(chatColors.getChatBubbleMask());
|
||||
}
|
||||
|
||||
private void showBlockedDialog(@NonNull Recipient recipient) {
|
||||
|
|
|
@ -3,11 +3,13 @@ package org.thoughtcrime.securesms.contacts.avatars;
|
|||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
|
||||
public interface FallbackContactPhoto {
|
||||
|
||||
public Drawable asDrawable(Context context, int color);
|
||||
public Drawable asDrawable(Context context, int color, boolean inverted);
|
||||
public Drawable asSmallDrawable(Context context, int color, boolean inverted);
|
||||
public Drawable asCallCard(Context context);
|
||||
|
||||
Drawable asDrawable(@NonNull Context context, @NonNull ChatColors chatColors);
|
||||
Drawable asDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted);
|
||||
Drawable asSmallDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted);
|
||||
Drawable asCallCard(@NonNull Context context);
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@ import android.graphics.drawable.LayerDrawable;
|
|||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
@ -26,18 +26,18 @@ public final class FallbackPhoto20dp implements FallbackContactPhoto {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color) {
|
||||
return buildDrawable(context, color);
|
||||
public Drawable asDrawable(@NonNull Context context, @NonNull ChatColors chatColors) {
|
||||
return buildDrawable(context, chatColors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color, boolean inverted) {
|
||||
return buildDrawable(context, color);
|
||||
public Drawable asDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
return buildDrawable(context, chatColors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asSmallDrawable(Context context, int color, boolean inverted) {
|
||||
return buildDrawable(context, color);
|
||||
public Drawable asSmallDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
return buildDrawable(context, chatColors);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -45,15 +45,13 @@ public final class FallbackPhoto20dp implements FallbackContactPhoto {
|
|||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private @NonNull Drawable buildDrawable(@NonNull Context context, int color) {
|
||||
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable))).mutate();
|
||||
private @NonNull Drawable buildDrawable(@NonNull Context context, @NonNull ChatColors backgroundColor) {
|
||||
Drawable background = backgroundColor.asCircle();
|
||||
Drawable foreground = AppCompatResources.getDrawable(context, drawable20dp);
|
||||
Drawable gradient = AppCompatResources.getDrawable(context, R.drawable.avatar_gradient);
|
||||
LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient});
|
||||
int foregroundInset = ViewUtil.dpToPx(2);
|
||||
|
||||
DrawableCompat.setTint(background, color);
|
||||
|
||||
drawable.setLayerInset(1, foregroundInset, foregroundInset, foregroundInset, foregroundInset);
|
||||
|
||||
return drawable;
|
||||
|
|
|
@ -10,49 +10,48 @@ import androidx.appcompat.content.res.AppCompatResources;
|
|||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class FallbackPhoto80dp implements FallbackContactPhoto {
|
||||
|
||||
@DrawableRes private final int drawable80dp;
|
||||
private final int backgroundColor;
|
||||
@DrawableRes private final int drawable80dp;
|
||||
private final ChatColors backgroundColor;
|
||||
|
||||
public FallbackPhoto80dp(@DrawableRes int drawable80dp, int backgroundColor) {
|
||||
public FallbackPhoto80dp(@DrawableRes int drawable80dp, @NonNull ChatColors backgroundColor) {
|
||||
this.drawable80dp = drawable80dp;
|
||||
this.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color) {
|
||||
public Drawable asDrawable(@NonNull Context context, @NonNull ChatColors chatColors) {
|
||||
return buildDrawable(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color, boolean inverted) {
|
||||
public Drawable asDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
return buildDrawable(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asSmallDrawable(Context context, int color, boolean inverted) {
|
||||
public Drawable asSmallDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asCallCard(Context context) {
|
||||
public Drawable asCallCard(@NonNull Context context) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private @NonNull Drawable buildDrawable(@NonNull Context context) {
|
||||
Drawable background = DrawableCompat.wrap(Objects.requireNonNull(AppCompatResources.getDrawable(context, R.drawable.circle_tintable))).mutate();
|
||||
Drawable background = backgroundColor.asCircle();
|
||||
Drawable foreground = AppCompatResources.getDrawable(context, drawable80dp);
|
||||
Drawable gradient = AppCompatResources.getDrawable(context, R.drawable.avatar_gradient);
|
||||
LayerDrawable drawable = new LayerDrawable(new Drawable[]{background, foreground, gradient});
|
||||
int foregroundInset = ViewUtil.dpToPx(24);
|
||||
|
||||
DrawableCompat.setTint(background, backgroundColor);
|
||||
|
||||
drawable.setLayerInset(1, foregroundInset, foregroundInset, foregroundInset, foregroundInset);
|
||||
|
||||
return drawable;
|
||||
|
|
|
@ -15,6 +15,7 @@ import androidx.appcompat.content.res.AppCompatResources;
|
|||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
|
@ -42,12 +43,12 @@ public class GeneratedContactPhoto implements FallbackContactPhoto {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color) {
|
||||
return asDrawable(context, color,false);
|
||||
public Drawable asDrawable(@NonNull Context context, @NonNull ChatColors chatColors) {
|
||||
return asDrawable(context, chatColors, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color, boolean inverted) {
|
||||
public Drawable asDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
int targetSize = this.targetSize != -1
|
||||
? this.targetSize
|
||||
: context.getResources().getDimensionPixelSize(R.dimen.contact_photo_target_size);
|
||||
|
@ -55,34 +56,36 @@ public class GeneratedContactPhoto implements FallbackContactPhoto {
|
|||
String character = getAbbreviation(name);
|
||||
|
||||
if (!TextUtils.isEmpty(character)) {
|
||||
Drawable background = chatColors.asCircle();
|
||||
|
||||
Drawable base = TextDrawable.builder()
|
||||
.beginConfig()
|
||||
.width(targetSize)
|
||||
.height(targetSize)
|
||||
.useFont(TYPEFACE)
|
||||
.fontSize(fontSize)
|
||||
.textColor(inverted ? color : Color.WHITE)
|
||||
.textColor(inverted ? chatColors.asSingleColor() : Color.WHITE)
|
||||
.endConfig()
|
||||
.buildRound(character, inverted ? Color.WHITE : color);
|
||||
.buildRound(character, inverted ? Color.WHITE : Color.TRANSPARENT);
|
||||
|
||||
Drawable gradient = ContextUtil.requireDrawable(context, R.drawable.avatar_gradient);
|
||||
return new LayerDrawable(new Drawable[] { base, gradient });
|
||||
return new LayerDrawable(new Drawable[] { background, base, gradient });
|
||||
}
|
||||
|
||||
return newFallbackDrawable(context, color, inverted);
|
||||
return newFallbackDrawable(context, chatColors, inverted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asSmallDrawable(Context context, int color, boolean inverted) {
|
||||
return asDrawable(context, color, inverted);
|
||||
public Drawable asSmallDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
return asDrawable(context, chatColors, inverted);
|
||||
}
|
||||
|
||||
protected @DrawableRes int getFallbackResId() {
|
||||
return fallbackResId;
|
||||
}
|
||||
|
||||
protected Drawable newFallbackDrawable(@NonNull Context context, int color, boolean inverted) {
|
||||
return new ResourceContactPhoto(fallbackResId).asDrawable(context, color, inverted);
|
||||
protected Drawable newFallbackDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
return new ResourceContactPhoto(fallbackResId).asDrawable(context, chatColors, inverted);
|
||||
}
|
||||
|
||||
private @Nullable String getAbbreviation(String name) {
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.amulyakhare.textdrawable.TextDrawable;
|
|||
import com.makeramen.roundedimageview.RoundedDrawable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
|
||||
public class ResourceContactPhoto implements FallbackContactPhoto {
|
||||
|
@ -45,29 +46,29 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
|
|||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Drawable asDrawable(@NonNull Context context, int color) {
|
||||
return asDrawable(context, color, false);
|
||||
public @NonNull Drawable asDrawable(@NonNull Context context, @NonNull ChatColors chatColors) {
|
||||
return asDrawable(context, chatColors, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Drawable asDrawable(@NonNull Context context, int color, boolean inverted) {
|
||||
return buildDrawable(context, resourceId, color, inverted);
|
||||
public @NonNull Drawable asDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
return buildDrawable(context, resourceId, chatColors, inverted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Drawable asSmallDrawable(@NonNull Context context, int color, boolean inverted) {
|
||||
return buildDrawable(context, smallResourceId, color, inverted);
|
||||
public @NonNull Drawable asSmallDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
return buildDrawable(context, smallResourceId, chatColors, inverted);
|
||||
}
|
||||
|
||||
private @NonNull Drawable buildDrawable(@NonNull Context context, int resourceId, int color, boolean inverted) {
|
||||
Drawable background = TextDrawable.builder().buildRound(" ", inverted ? Color.WHITE : color);
|
||||
private @NonNull Drawable buildDrawable(@NonNull Context context, int resourceId, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
Drawable background = chatColors.asCircle();
|
||||
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId));
|
||||
|
||||
//noinspection ConstantConditions
|
||||
foreground.setScaleType(scaleType);
|
||||
|
||||
if (inverted) {
|
||||
foreground.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
|
||||
foreground.setColorFilter(chatColors.asSingleColor(), PorterDuff.Mode.SRC_ATOP);
|
||||
}
|
||||
|
||||
Drawable gradient = ContextUtil.requireDrawable(context, R.drawable.avatar_gradient);
|
||||
|
|
|
@ -3,33 +3,35 @@ package org.thoughtcrime.securesms.contacts.avatars;
|
|||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.makeramen.roundedimageview.RoundedDrawable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
|
||||
public class TransparentContactPhoto implements FallbackContactPhoto {
|
||||
|
||||
public TransparentContactPhoto() {}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color) {
|
||||
return asDrawable(context, color, false);
|
||||
public Drawable asDrawable(@NonNull Context context, @NonNull ChatColors chatColors) {
|
||||
return asDrawable(context, chatColors, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asDrawable(Context context, int color, boolean inverted) {
|
||||
public Drawable asDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
return RoundedDrawable.fromDrawable(context.getResources().getDrawable(android.R.color.transparent));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asSmallDrawable(Context context, int color, boolean inverted) {
|
||||
return asDrawable(context, color, inverted);
|
||||
public Drawable asSmallDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
return asDrawable(context, chatColors, inverted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Drawable asCallCard(Context context) {
|
||||
public Drawable asCallCard(@NonNull Context context) {
|
||||
return ContextCompat.getDrawable(context, R.drawable.ic_contact_picture_large);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Path
|
||||
import android.graphics.Region
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
|
||||
/**
|
||||
* Drawable which clips out the given projection
|
||||
*/
|
||||
class ClipProjectionDrawable(wrapped: Drawable) : LayerDrawable(arrayOf(wrapped)) {
|
||||
|
||||
constructor() : this(ColorDrawable(Color.TRANSPARENT))
|
||||
|
||||
init {
|
||||
setId(0, 0)
|
||||
}
|
||||
|
||||
private val clipPath = Path()
|
||||
private var projections: List<Projection> = listOf()
|
||||
|
||||
fun setWrappedDrawable(drawable: Drawable) {
|
||||
setDrawableByLayerId(0, drawable)
|
||||
}
|
||||
|
||||
fun setProjections(projections: Set<Projection>) {
|
||||
this.projections = projections.toList()
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
fun clearProjections() {
|
||||
this.projections = listOf()
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (projections.isNotEmpty()) {
|
||||
canvas.save()
|
||||
clipPath.rewind()
|
||||
|
||||
projections.forEach {
|
||||
it.applyToPath(clipPath)
|
||||
}
|
||||
|
||||
canvas.clipPath(clipPath, Region.Op.DIFFERENCE)
|
||||
super.draw(canvas)
|
||||
canvas.restore()
|
||||
} else {
|
||||
super.draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1097,7 +1097,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport());
|
||||
break;
|
||||
case GIF:
|
||||
AttachmentManager.selectGif(this, PICK_GIF, !isSecureText, recipient.get().getColor().toConversationColor(this));
|
||||
AttachmentManager.selectGif(this, PICK_GIF, !isSecureText, recipient.get().getChatColors().asSingleColor());
|
||||
break;
|
||||
case FILE:
|
||||
AttachmentManager.selectDocument(this, PICK_DOCUMENT);
|
||||
|
@ -1252,7 +1252,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
GlideApp.with(this)
|
||||
.asBitmap()
|
||||
.load(recipient.getContactPhoto())
|
||||
.error(recipient.getFallbackContactPhoto().asDrawable(this, recipient.getColor().toAvatarColor(this), false))
|
||||
.error(recipient.getFallbackContactPhoto().asDrawable(this, recipient.getChatColors(), false))
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||
|
@ -3506,7 +3506,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
messageRecord.getDateSent(),
|
||||
author,
|
||||
body,
|
||||
slideDeck);
|
||||
slideDeck,
|
||||
fragment.getColorizer());
|
||||
|
||||
} else if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) {
|
||||
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
|
||||
|
@ -3520,7 +3521,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
messageRecord.getDateSent(),
|
||||
author,
|
||||
conversationMessage.getDisplayBody(this),
|
||||
slideDeck);
|
||||
slideDeck,
|
||||
fragment.getColorizer());
|
||||
} else {
|
||||
SlideDeck slideDeck = messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck();
|
||||
|
||||
|
@ -3534,7 +3536,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
messageRecord.getDateSent(),
|
||||
author,
|
||||
conversationMessage.getDisplayBody(this),
|
||||
slideDeck);
|
||||
slideDeck,
|
||||
fragment.getColorizer());
|
||||
}
|
||||
|
||||
inputPanel.clickOnComposeInput();
|
||||
|
|
|
@ -43,11 +43,12 @@ import org.signal.core.util.logging.Log;
|
|||
import org.signal.paging.PagingController;
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.MaskView;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -123,13 +124,15 @@ public class ConversationAdapter
|
|||
private boolean hasWallpaper;
|
||||
private boolean isMessageRequestAccepted;
|
||||
private ConversationMessage inlineContent;
|
||||
private Colorizer colorizer;
|
||||
|
||||
ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@NonNull Locale locale,
|
||||
@Nullable ItemClickListener clickListener,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory)
|
||||
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||
@NonNull Colorizer colorizer)
|
||||
{
|
||||
super(new DiffUtil.ItemCallback<ConversationMessage>() {
|
||||
@Override
|
||||
|
@ -157,6 +160,7 @@ public class ConversationAdapter
|
|||
this.hasWallpaper = recipient.hasWallpaper();
|
||||
this.isMessageRequestAccepted = true;
|
||||
this.attachmentMediaSourceFactory = attachmentMediaSourceFactory;
|
||||
this.colorizer = colorizer;
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
@ -271,7 +275,8 @@ public class ConversationAdapter
|
|||
hasWallpaper,
|
||||
isMessageRequestAccepted,
|
||||
attachmentMediaSourceFactory,
|
||||
conversationMessage == inlineContent);
|
||||
conversationMessage == inlineContent,
|
||||
colorizer);
|
||||
|
||||
if (conversationMessage == recordToPulse) {
|
||||
recordToPulse = null;
|
||||
|
@ -635,7 +640,7 @@ public class ConversationAdapter
|
|||
}
|
||||
}
|
||||
|
||||
final static class ConversationViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable {
|
||||
final static class ConversationViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable, Colorizable {
|
||||
public ConversationViewHolder(final @NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
@ -665,7 +670,7 @@ public class ConversationAdapter
|
|||
}
|
||||
|
||||
@NonNull
|
||||
public @Override GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerView) {
|
||||
public @Override Projection getProjection(@NonNull ViewGroup recyclerView) {
|
||||
return getBindable().getProjection(recyclerView);
|
||||
}
|
||||
|
||||
|
@ -673,6 +678,11 @@ public class ConversationAdapter
|
|||
public boolean canPlayContent() {
|
||||
return getBindable().canPlayContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<Projection> getColorizerProjections() {
|
||||
return getBindable().getColorizerProjections();
|
||||
}
|
||||
}
|
||||
|
||||
static class StickyHeaderViewHolder extends RecyclerView.ViewHolder {
|
||||
|
|
|
@ -89,6 +89,8 @@ import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity;
|
|||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ColorizerView;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
|
@ -100,9 +102,10 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
|||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionPlayerHolder;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionRecycler;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||
|
@ -183,39 +186,41 @@ public class ConversationFragment extends LoggingFragment {
|
|||
|
||||
private ConversationFragmentListener listener;
|
||||
|
||||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
private boolean isReacting;
|
||||
private ActionMode actionMode;
|
||||
private Locale locale;
|
||||
private FrameLayout videoContainer;
|
||||
private RecyclerView list;
|
||||
private RecyclerView.ItemDecoration lastSeenDecoration;
|
||||
private RecyclerView.ItemDecoration inlineDateDecoration;
|
||||
private ViewSwitcher topLoadMoreView;
|
||||
private ViewSwitcher bottomLoadMoreView;
|
||||
private ConversationTypingView typingView;
|
||||
private View composeDivider;
|
||||
private ConversationScrollToView scrollToBottomButton;
|
||||
private ConversationScrollToView scrollToMentionButton;
|
||||
private TextView scrollDateHeader;
|
||||
private ConversationBannerView conversationBanner;
|
||||
private MessageRequestViewModel messageRequestViewModel;
|
||||
private MessageCountsViewModel messageCountsViewModel;
|
||||
private ConversationViewModel conversationViewModel;
|
||||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
private MarkReadHelper markReadHelper;
|
||||
private Animation scrollButtonInAnimation;
|
||||
private Animation mentionButtonInAnimation;
|
||||
private Animation scrollButtonOutAnimation;
|
||||
private Animation mentionButtonOutAnimation;
|
||||
private OnScrollListener conversationScrollListener;
|
||||
private int pulsePosition = -1;
|
||||
private VoiceNoteMediaController voiceNoteMediaController;
|
||||
private View toolbarShadow;
|
||||
private Stopwatch startupStopwatch;
|
||||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
private boolean isReacting;
|
||||
private ActionMode actionMode;
|
||||
private Locale locale;
|
||||
private FrameLayout videoContainer;
|
||||
private RecyclerView list;
|
||||
private RecyclerView.ItemDecoration lastSeenDecoration;
|
||||
private RecyclerView.ItemDecoration inlineDateDecoration;
|
||||
private ViewSwitcher topLoadMoreView;
|
||||
private ViewSwitcher bottomLoadMoreView;
|
||||
private ConversationTypingView typingView;
|
||||
private View composeDivider;
|
||||
private ConversationScrollToView scrollToBottomButton;
|
||||
private ConversationScrollToView scrollToMentionButton;
|
||||
private TextView scrollDateHeader;
|
||||
private ConversationBannerView conversationBanner;
|
||||
private MessageRequestViewModel messageRequestViewModel;
|
||||
private MessageCountsViewModel messageCountsViewModel;
|
||||
private ConversationViewModel conversationViewModel;
|
||||
private SnapToTopDataObserver snapToTopDataObserver;
|
||||
private MarkReadHelper markReadHelper;
|
||||
private Animation scrollButtonInAnimation;
|
||||
private Animation mentionButtonInAnimation;
|
||||
private Animation scrollButtonOutAnimation;
|
||||
private Animation mentionButtonOutAnimation;
|
||||
private OnScrollListener conversationScrollListener;
|
||||
private int pulsePosition = -1;
|
||||
private VoiceNoteMediaController voiceNoteMediaController;
|
||||
private View toolbarShadow;
|
||||
private ColorizerView colorizerView;
|
||||
private Stopwatch startupStopwatch;
|
||||
|
||||
private GiphyMp4ProjectionRecycler giphyMp4ProjectionRecycler;
|
||||
private Colorizer colorizer;
|
||||
|
||||
public static void prepare(@NonNull Context context) {
|
||||
FrameLayout parent = new FrameLayout(context);
|
||||
|
@ -247,6 +252,10 @@ public class ConversationFragment extends LoggingFragment {
|
|||
scrollToMentionButton = view.findViewById(R.id.scroll_to_mention);
|
||||
scrollDateHeader = view.findViewById(R.id.scroll_date_header);
|
||||
toolbarShadow = requireActivity().findViewById(R.id.conversation_toolbar_shadow);
|
||||
colorizerView = view.findViewById(R.id.conversation_colorizer_view);
|
||||
|
||||
ConversationIntents.Args args = ConversationIntents.Args.from(requireActivity().getIntent());
|
||||
colorizerView.setBackground(args.getChatColors().getChatBubbleMask());
|
||||
|
||||
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
|
||||
list.setHasFixedSize(false);
|
||||
|
@ -272,7 +281,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
conversationMessage.getMessageRecord(),
|
||||
messageRequestViewModel.shouldShowMessageRequest()),
|
||||
this::handleReplyMessage,
|
||||
giphyMp4ProjectionRecycler
|
||||
this::onViewHolderPositionTranslated
|
||||
).attachToRecyclerView(list);
|
||||
|
||||
setupListLayoutListeners();
|
||||
|
@ -310,6 +319,19 @@ public class ConversationFragment extends LoggingFragment {
|
|||
|
||||
updateToolbarDependentMargins();
|
||||
|
||||
colorizer = new Colorizer(colorizerView);
|
||||
colorizer.attachToRecyclerView(list);
|
||||
|
||||
conversationViewModel.getChatColors().observe(getViewLifecycleOwner(), chatColors -> colorizer.onChatColorsChanged(chatColors));
|
||||
conversationViewModel.getNameColorsMap().observe(getViewLifecycleOwner(), nameColorsMap -> {
|
||||
colorizer.onNameColorsChanged(nameColorsMap);
|
||||
|
||||
ConversationAdapter adapter = getListAdapter();
|
||||
if (adapter != null) {
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
@ -352,10 +374,12 @@ public class ConversationFragment extends LoggingFragment {
|
|||
private void setListVerticalTranslation() {
|
||||
if (list.canScrollVertically(1) || list.canScrollVertically(-1) || list.getChildCount() == 0) {
|
||||
list.setTranslationY(0);
|
||||
colorizerView.setTranslationY(0);
|
||||
list.setOverScrollMode(RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS);
|
||||
} else {
|
||||
int chTop = list.getChildAt(list.getChildCount() - 1).getTop();
|
||||
list.setTranslationY(Math.min(0, -chTop));
|
||||
colorizerView.setTranslationY(Math.min(0, -chTop));
|
||||
list.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER);
|
||||
}
|
||||
|
||||
|
@ -467,6 +491,16 @@ public class ConversationFragment extends LoggingFragment {
|
|||
}
|
||||
}
|
||||
|
||||
private void onViewHolderPositionTranslated(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (viewHolder instanceof GiphyMp4Playable) {
|
||||
giphyMp4ProjectionRecycler.updateDisplay(recyclerView, (GiphyMp4Playable) viewHolder);
|
||||
}
|
||||
|
||||
if (colorizer != null) {
|
||||
colorizer.applyClipPathsToMaskedGradient(recyclerView);
|
||||
}
|
||||
}
|
||||
|
||||
private int getStartPosition() {
|
||||
return conversationViewModel.getArgs().getStartingPosition();
|
||||
}
|
||||
|
@ -616,7 +650,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
}
|
||||
|
||||
Log.d(TAG, "Initializing adapter for " + recipient.getId());
|
||||
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get(), new AttachmentMediaSourceFactory(requireContext()));
|
||||
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get(), new AttachmentMediaSourceFactory(requireContext()), colorizer);
|
||||
adapter.setPagingController(conversationViewModel.getPagingController());
|
||||
list.setAdapter(adapter);
|
||||
setInlineDateDecoration(adapter);
|
||||
|
@ -1124,6 +1158,10 @@ public class ConversationFragment extends LoggingFragment {
|
|||
}
|
||||
}
|
||||
|
||||
public @NonNull Colorizer getColorizer() {
|
||||
return Objects.requireNonNull(colorizer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
public void jumpToMessage(@NonNull RecipientId author, long timestamp, @Nullable Runnable onMessageNotFound) {
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.net.Uri;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -67,8 +68,8 @@ public class ConversationIntents {
|
|||
private final StickerLocator stickerLocator;
|
||||
private final boolean isBorderless;
|
||||
private final int distributionType;
|
||||
private final int startingPosition;
|
||||
private final boolean firstTimeInSelfCreatedGroup;
|
||||
private final int startingPosition;
|
||||
private final boolean firstTimeInSelfCreatedGroup;
|
||||
|
||||
static Args from(@NonNull Intent intent) {
|
||||
if (isBubbleIntent(intent)) {
|
||||
|
@ -155,6 +156,10 @@ public class ConversationIntents {
|
|||
// TODO [greyson][wallpaper] Is it worth it to do this beforehand?
|
||||
return Recipient.resolved(recipientId).getWallpaper();
|
||||
}
|
||||
|
||||
public @NonNull ChatColors getChatColors() {
|
||||
return Recipient.resolved(recipientId).getChatColors();
|
||||
}
|
||||
}
|
||||
|
||||
public final static class Builder {
|
||||
|
|
|
@ -55,8 +55,8 @@ import androidx.annotation.Nullable;
|
|||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
|
||||
|
@ -72,7 +72,6 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
|
|||
import org.thoughtcrime.securesms.components.BorderlessImageView;
|
||||
import org.thoughtcrime.securesms.components.ConversationItemFooter;
|
||||
import org.thoughtcrime.securesms.components.ConversationItemThumbnail;
|
||||
import org.thoughtcrime.securesms.components.CornerMask;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.components.LinkPreviewView;
|
||||
import org.thoughtcrime.securesms.components.Outliner;
|
||||
|
@ -81,6 +80,7 @@ import org.thoughtcrime.securesms.components.SharedContactView;
|
|||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
|
@ -92,7 +92,6 @@ import org.thoughtcrime.securesms.database.model.Quote;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||
|
@ -121,9 +120,9 @@ import org.thoughtcrime.securesms.util.DynamicTheme;
|
|||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||
import org.thoughtcrime.securesms.util.StringUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.UrlClickHandler;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.VibrateUtil;
|
||||
|
@ -135,6 +134,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
@ -160,13 +160,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
|
||||
private static final Rect SWIPE_RECT = new Rect();
|
||||
|
||||
private ConversationMessage conversationMessage;
|
||||
private MessageRecord messageRecord;
|
||||
private Locale locale;
|
||||
private boolean groupThread;
|
||||
private LiveRecipient recipient;
|
||||
private GlideRequests glideRequests;
|
||||
private ValueAnimator pulseOutlinerAlphaAnimator;
|
||||
private ClipProjectionDrawable backgroundDrawable;
|
||||
private ConversationMessage conversationMessage;
|
||||
private MessageRecord messageRecord;
|
||||
private Locale locale;
|
||||
private boolean groupThread;
|
||||
private LiveRecipient recipient;
|
||||
private GlideRequests glideRequests;
|
||||
private ValueAnimator pulseOutlinerAlphaAnimator;
|
||||
|
||||
protected ConversationItemBodyBubble bodyBubble;
|
||||
protected View reply;
|
||||
|
@ -212,8 +213,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
|
||||
private final Context context;
|
||||
|
||||
private MediaSource mediaSource;
|
||||
private boolean canPlayContent;
|
||||
private MediaSource mediaSource;
|
||||
private boolean canPlayContent;
|
||||
private Projection.Corners bodyBubbleCorners;
|
||||
private Colorizer colorizer;
|
||||
private boolean hasWallpaper;
|
||||
|
||||
public ConversationItem(Context context) {
|
||||
this(context, null);
|
||||
|
@ -235,6 +239,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
|
||||
initializeAttributes();
|
||||
|
||||
this.backgroundDrawable = new ClipProjectionDrawable(Objects.requireNonNull(ContextCompat.getDrawable(getContext(),
|
||||
R.drawable.conversation_item_background)));
|
||||
this.bodyText = findViewById(R.id.conversation_item_body);
|
||||
this.footer = findViewById(R.id.conversation_item_footer);
|
||||
this.stickerFooter = findViewById(R.id.conversation_item_sticker_footer);
|
||||
|
@ -276,7 +282,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
boolean hasWallpaper,
|
||||
boolean isMessageRequestAccepted,
|
||||
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||
boolean allowedToPlayInline)
|
||||
boolean allowedToPlayInline,
|
||||
@NonNull Colorizer colorizer)
|
||||
{
|
||||
if (this.recipient != null) this.recipient.removeForeverObserver(this);
|
||||
if (this.conversationRecipient != null) this.conversationRecipient.removeForeverObserver(this);
|
||||
|
@ -293,6 +300,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
this.recipient = messageRecord.getIndividualRecipient().live();
|
||||
this.canPlayContent = false;
|
||||
this.mediaSource = null;
|
||||
this.colorizer = colorizer;
|
||||
|
||||
this.recipient.observeForever(this);
|
||||
this.conversationRecipient.observeForever(this);
|
||||
|
@ -301,14 +309,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper, isMessageRequestAccepted, attachmentMediaSourceFactory, allowedToPlayInline);
|
||||
setBodyText(messageRecord, searchQuery, isMessageRequestAccepted);
|
||||
setBubbleState(messageRecord, hasWallpaper);
|
||||
setBubbleState(messageRecord, messageRecord.getRecipient(), hasWallpaper, colorizer);
|
||||
setInteractionState(conversationMessage, pulse);
|
||||
setStatusIcons(messageRecord, hasWallpaper);
|
||||
setContactPhoto(recipient.get());
|
||||
setGroupMessageStatus(messageRecord, recipient.get());
|
||||
setGroupAuthorColor(messageRecord, hasWallpaper);
|
||||
setGroupAuthorColor(messageRecord, hasWallpaper, colorizer);
|
||||
setAuthor(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper);
|
||||
setQuote(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||
setQuote(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, colorizer);
|
||||
setMessageSpacing(context, messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
|
||||
setReactions(messageRecord);
|
||||
setFooter(messageRecord, nextMessageRecord, locale, groupThread, hasWallpaper);
|
||||
|
@ -390,7 +398,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
|
||||
@Override
|
||||
public void onRecipientChanged(@NonNull Recipient modified) {
|
||||
setBubbleState(messageRecord, modified.hasWallpaper());
|
||||
setBubbleState(messageRecord, modified, modified.hasWallpaper(), colorizer);
|
||||
if (recipient.getId().equals(modified.getId())) {
|
||||
setContactPhoto(modified);
|
||||
setGroupMessageStatus(messageRecord, modified);
|
||||
|
@ -438,11 +446,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
|
||||
/// MessageRecord Attribute Parsers
|
||||
|
||||
private void setBubbleState(MessageRecord messageRecord, boolean hasWallpaper) {
|
||||
private void setBubbleState(MessageRecord messageRecord, @NonNull Recipient recipient, boolean hasWallpaper, @NonNull Colorizer colorizer) {
|
||||
this.hasWallpaper = hasWallpaper;
|
||||
|
||||
bodyText.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_text_primary));
|
||||
bodyText.setLinkTextColor(ContextCompat.getColor(getContext(), R.color.signal_text_primary));
|
||||
|
||||
if (messageRecord.isOutgoing() && !messageRecord.isRemoteDelete()) {
|
||||
bodyBubble.getBackground().setColorFilter(getDefaultBubbleColor(hasWallpaper), PorterDuff.Mode.MULTIPLY);
|
||||
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
footer.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
|
||||
bodyBubble.getBackground().setColorFilter(recipient.getChatColors().getChatBubbleColorFilter());
|
||||
bodyText.setTextColor(colorizer.getOutgoingBodyTextColor(context));
|
||||
bodyText.setLinkTextColor(colorizer.getOutgoingBodyTextColor(context));
|
||||
footer.setTextColor(colorizer.getOutgoingFooterTextColor(context));
|
||||
footer.setIconColor(colorizer.getOutgoingFooterIconColor(context));
|
||||
footer.setOnlyShowSendingStatus(false, messageRecord);
|
||||
} else if (messageRecord.isRemoteDelete() || (isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord))) {
|
||||
if (hasWallpaper) {
|
||||
|
@ -454,9 +469,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
footer.setOnlyShowSendingStatus(messageRecord.isRemoteDelete(), messageRecord);
|
||||
} else {
|
||||
bodyBubble.getBackground().setColorFilter(messageRecord.getRecipient().getColor().toConversationColor(context), PorterDuff.Mode.MULTIPLY);
|
||||
footer.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_secondary_color));
|
||||
footer.setIconColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_secondary_color));
|
||||
bodyBubble.getBackground().setColorFilter(getDefaultBubbleColor(hasWallpaper), PorterDuff.Mode.SRC_IN);
|
||||
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
footer.setIconColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
|
||||
footer.setOnlyShowSendingStatus(false, messageRecord);
|
||||
}
|
||||
|
||||
|
@ -490,7 +505,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
|
||||
private void setAudioViewTint(MessageRecord messageRecord) {
|
||||
if (hasAudio(messageRecord)) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
if (!messageRecord.isOutgoing()) {
|
||||
if (DynamicTheme.isDarkTheme(context)) {
|
||||
audioViewStub.get().setTint(Color.WHITE);
|
||||
} else {
|
||||
|
@ -504,7 +519,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
|
||||
private void setInteractionState(ConversationMessage conversationMessage, boolean pulseMention) {
|
||||
if (batchSelected.contains(conversationMessage)) {
|
||||
setBackgroundResource(R.drawable.conversation_item_background);
|
||||
setBackground(backgroundDrawable);
|
||||
setSelected(true);
|
||||
} else if (pulseMention) {
|
||||
setBackground(null);
|
||||
|
@ -683,9 +698,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
}
|
||||
|
||||
if (messageRecord.isOutgoing()) {
|
||||
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, isDarkTheme(context) ? R.color.core_grey_60 : R.color.core_grey_20));
|
||||
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_25));
|
||||
} else {
|
||||
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, R.color.transparent_black_40));
|
||||
bodyText.setMentionBackgroundTint(ContextCompat.getColor(context, isDarkTheme(context) ? R.color.core_grey_60 : R.color.core_grey_20));
|
||||
}
|
||||
|
||||
bodyText.setText(StringUtil.trim(styledText));
|
||||
|
@ -704,6 +719,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
{
|
||||
boolean showControls = !messageRecord.isFailed();
|
||||
|
||||
bodyBubble.setQuoteViewProjection(null);
|
||||
bodyBubble.setVideoPlayerProjection(null);
|
||||
updateBackgroundDrawableProjections();
|
||||
|
||||
if (eventListener != null && audioViewStub.resolved()) {
|
||||
Log.d(TAG, "setMediaAttributes: unregistering voice note callbacks for audio slide " + audioViewStub.get().getAudioSlideUri());
|
||||
eventListener.onUnregisterVoiceNoteCallbacks(audioViewStub.get().getPlaybackStateObserver());
|
||||
|
@ -875,8 +894,13 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener);
|
||||
mediaThumbnailStub.get().setOnClickListener(passthroughClickListener);
|
||||
mediaThumbnailStub.get().showShade(TextUtils.isEmpty(messageRecord.getDisplayBody(getContext())) && !hasExtraText(messageRecord));
|
||||
mediaThumbnailStub.get().setConversationColor(messageRecord.isOutgoing() ? getDefaultBubbleColor(hasWallpaper)
|
||||
: messageRecord.getRecipient().getColor().toConversationColor(context));
|
||||
|
||||
if (!messageRecord.isOutgoing()) {
|
||||
mediaThumbnailStub.get().setConversationColor(getDefaultBubbleColor(hasWallpaper));
|
||||
} else {
|
||||
mediaThumbnailStub.get().setConversationColor(Color.TRANSPARENT);
|
||||
}
|
||||
|
||||
mediaThumbnailStub.get().setBorderless(false);
|
||||
|
||||
setThumbnailCorners(messageRecord, previousRecord, nextRecord, isGroupThread);
|
||||
|
@ -1068,14 +1092,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
}
|
||||
}
|
||||
|
||||
private void setQuote(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
|
||||
private void setQuote(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread, @NonNull Colorizer colorizer) {
|
||||
if (current.isMms() && !current.isMmsNotification() && ((MediaMmsMessageRecord)current).getQuote() != null) {
|
||||
if (quoteView == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
Quote quote = ((MediaMmsMessageRecord)current).getQuote();
|
||||
//noinspection ConstantConditions
|
||||
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getDisplayText(), quote.isOriginalMissing(), quote.getAttachment());
|
||||
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getDisplayText(), quote.isOriginalMissing(), quote.getAttachment(), colorizer);
|
||||
quoteView.setVisibility(View.VISIBLE);
|
||||
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
|
@ -1177,11 +1201,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
|
||||
if (hasWallpaper && hasNoBubble((messageRecord))) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
activeFooter.enableBubbleBackground(R.drawable.wallpaper_bubble_background_tintable_11, getDefaultBubbleColor(hasWallpaper));
|
||||
activeFooter.disableBubbleBackground();
|
||||
activeFooter.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_sent_text_secondary_color));
|
||||
activeFooter.setIconColor(ContextCompat.getColor(context, R.color.conversation_item_sent_text_secondary_color));
|
||||
} else {
|
||||
activeFooter.enableBubbleBackground(R.drawable.wallpaper_bubble_background_tintable_11, messageRecord.getRecipient().getColor().toConversationColor(context));
|
||||
activeFooter.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_secondary_color));
|
||||
activeFooter.setIconColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_secondary_color));
|
||||
activeFooter.enableBubbleBackground(R.drawable.wallpaper_bubble_background_tintable_11, getDefaultBubbleColor(hasWallpaper));
|
||||
}
|
||||
} else if (hasNoBubble(messageRecord)){
|
||||
activeFooter.disableBubbleBackground();
|
||||
|
@ -1228,17 +1252,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
}
|
||||
}
|
||||
|
||||
private void setGroupAuthorColor(@NonNull MessageRecord messageRecord, boolean hasWallpaper) {
|
||||
private void setGroupAuthorColor(@NonNull MessageRecord messageRecord, boolean hasWallpaper, @NonNull Colorizer colorizer) {
|
||||
if (groupSender != null) {
|
||||
int stickerAuthorColor = ContextCompat.getColor(context, R.color.signal_text_primary);
|
||||
|
||||
if (shouldDrawBodyBubbleOutline(messageRecord, false)) {
|
||||
groupSender.setTextColor(stickerAuthorColor);
|
||||
} else if (!hasWallpaper && hasNoBubble(messageRecord)) {
|
||||
groupSender.setTextColor(stickerAuthorColor);
|
||||
} else {
|
||||
groupSender.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_primary_color));
|
||||
}
|
||||
groupSender.setTextColor(colorizer.getIncomingGroupSenderColor(getContext(), messageRecord.getIndividualRecipient()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1254,7 +1270,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
|
||||
if (hasWallpaper && hasNoBubble(current)) {
|
||||
groupSenderHolder.setBackgroundResource(R.drawable.wallpaper_bubble_background_tintable_11);
|
||||
groupSenderHolder.getBackground().setColorFilter(messageRecord.getRecipient().getColor().toConversationColor(context), PorterDuff.Mode.MULTIPLY);
|
||||
groupSenderHolder.getBackground().setColorFilter(getDefaultBubbleColor(hasWallpaper), PorterDuff.Mode.MULTIPLY);
|
||||
} else {
|
||||
groupSenderHolder.setBackground(null);
|
||||
}
|
||||
|
@ -1297,40 +1313,48 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
background = R.drawable.message_bubble_background_sent_alone;
|
||||
outliner.setRadius(bigRadius);
|
||||
pulseOutliner.setRadius(bigRadius);
|
||||
bodyBubbleCorners = new Projection.Corners(bigRadius);
|
||||
} else {
|
||||
background = R.drawable.message_bubble_background_received_alone;
|
||||
outliner.setRadius(bigRadius);
|
||||
pulseOutliner.setRadius(bigRadius);
|
||||
bodyBubbleCorners = null;
|
||||
}
|
||||
} else if (isStartOfMessageCluster(current, previous, isGroupThread)) {
|
||||
if (current.isOutgoing()) {
|
||||
background = R.drawable.message_bubble_background_sent_start;
|
||||
setOutlinerRadii(outliner, bigRadius, bigRadius, smallRadius, bigRadius);
|
||||
setOutlinerRadii(pulseOutliner, bigRadius, bigRadius, smallRadius, bigRadius);
|
||||
bodyBubbleCorners = new Projection.Corners(bigRadius, bigRadius, smallRadius, bigRadius);
|
||||
} else {
|
||||
background = R.drawable.message_bubble_background_received_start;
|
||||
setOutlinerRadii(outliner, bigRadius, bigRadius, bigRadius, smallRadius);
|
||||
setOutlinerRadii(pulseOutliner, bigRadius, bigRadius, bigRadius, smallRadius);
|
||||
bodyBubbleCorners = null;
|
||||
}
|
||||
} else if (isEndOfMessageCluster(current, next, isGroupThread)) {
|
||||
if (current.isOutgoing()) {
|
||||
background = R.drawable.message_bubble_background_sent_end;
|
||||
setOutlinerRadii(outliner, bigRadius, smallRadius, bigRadius, bigRadius);
|
||||
setOutlinerRadii(pulseOutliner, bigRadius, smallRadius, bigRadius, bigRadius);
|
||||
bodyBubbleCorners = new Projection.Corners(bigRadius, smallRadius, bigRadius, bigRadius);
|
||||
} else {
|
||||
background = R.drawable.message_bubble_background_received_end;
|
||||
setOutlinerRadii(outliner, smallRadius, bigRadius, bigRadius, bigRadius);
|
||||
setOutlinerRadii(pulseOutliner, smallRadius, bigRadius, bigRadius, bigRadius);
|
||||
bodyBubbleCorners = null;
|
||||
}
|
||||
} else {
|
||||
if (current.isOutgoing()) {
|
||||
background = R.drawable.message_bubble_background_sent_middle;
|
||||
setOutlinerRadii(outliner, bigRadius, smallRadius, smallRadius, bigRadius);
|
||||
setOutlinerRadii(pulseOutliner, bigRadius, smallRadius, smallRadius, bigRadius);
|
||||
bodyBubbleCorners = new Projection.Corners(bigRadius, smallRadius, smallRadius, bigRadius);
|
||||
} else {
|
||||
background = R.drawable.message_bubble_background_received_middle;
|
||||
setOutlinerRadii(outliner, smallRadius, bigRadius, bigRadius, smallRadius);
|
||||
setOutlinerRadii(pulseOutliner, smallRadius, bigRadius, bigRadius, smallRadius);
|
||||
bodyBubbleCorners = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1440,7 +1464,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
public void showProjectionArea() {
|
||||
if (mediaThumbnailStub != null && mediaThumbnailStub.resolved()) {
|
||||
mediaThumbnailStub.get().showThumbnailView();
|
||||
bodyBubble.setMask(null);
|
||||
bodyBubble.setVideoPlayerProjection(null);
|
||||
updateBackgroundDrawableProjections();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1449,7 +1474,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
if (mediaThumbnailStub != null && mediaThumbnailStub.resolved()) {
|
||||
mediaThumbnailStub.get().hideThumbnailView();
|
||||
mediaThumbnailStub.get().getDrawingRect(thumbnailMaskingRect);
|
||||
bodyBubble.setMask(thumbnailMaskingRect);
|
||||
bodyBubble.setVideoPlayerProjection(Projection.relativeToViewWithCommonRoot(mediaThumbnailStub.get(), bodyBubble, null));
|
||||
updateBackgroundDrawableProjections();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1475,9 +1501,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
}
|
||||
|
||||
@Override
|
||||
public @NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerView) {
|
||||
return GiphyMp4Projection.forView(recyclerView, mediaThumbnailStub.get(), mediaThumbnailStub.get().getCornerMask())
|
||||
.translateX(bodyBubble.getTranslationX());
|
||||
public @NonNull Projection getProjection(@NonNull ViewGroup recyclerView) {
|
||||
return Projection.relativeToParent(recyclerView, mediaThumbnailStub.get(), mediaThumbnailStub.get().getCorners())
|
||||
.translateX(bodyBubble.getTranslationX());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1494,8 +1520,55 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
return rect;
|
||||
}
|
||||
|
||||
public @NonNull CornerMask getThumbnailCornerMask(@NonNull View view) {
|
||||
return new CornerMask(view, mediaThumbnailStub.get().getCornerMask());
|
||||
public @NonNull Projection.Corners getThumbnailCorners() {
|
||||
return mediaThumbnailStub.get().getCorners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<Projection> getColorizerProjections() {
|
||||
List<Projection> projections = new LinkedList<>();
|
||||
|
||||
if (messageRecord.isOutgoing() &&
|
||||
!hasNoBubble(messageRecord) &&
|
||||
bodyBubbleCorners != null)
|
||||
{
|
||||
projections.add(Projection.relativeToViewRoot(bodyBubble, bodyBubbleCorners).translateX(bodyBubble.getTranslationX()));
|
||||
}
|
||||
|
||||
if (messageRecord.isOutgoing() &&
|
||||
hasNoBubble(messageRecord) &&
|
||||
hasWallpaper)
|
||||
{
|
||||
Projection footerProjection = getActiveFooter(messageRecord).getProjection();
|
||||
if (footerProjection != null) {
|
||||
projections.add(footerProjection.translateX(bodyBubble.getTranslationX()));
|
||||
}
|
||||
}
|
||||
|
||||
if (!messageRecord.isOutgoing() &&
|
||||
hasQuote(messageRecord) &&
|
||||
quoteView != null)
|
||||
{
|
||||
bodyBubble.setQuoteViewProjection(quoteView.getProjection(bodyBubble));
|
||||
projections.add(quoteView.getProjection((ViewGroup) getRootView()).translateX(bodyBubble.getTranslationX()));
|
||||
}
|
||||
|
||||
return projections;
|
||||
}
|
||||
|
||||
private void updateBackgroundDrawableProjections() {
|
||||
Set<Projection> projections = Stream.of(bodyBubble.getProjections())
|
||||
.map(p -> Projection.translateFromDescendantToParentCoords(p, bodyBubble, this))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (messageRecord.isOutgoing() &&
|
||||
!hasNoBubble(messageRecord) &&
|
||||
bodyBubbleCorners != null)
|
||||
{
|
||||
projections.add(Projection.relativeToParent(this, bodyBubble, bodyBubbleCorners).translateX(bodyBubble.getTranslationX()));
|
||||
}
|
||||
|
||||
backgroundDrawable.setProjections(projections);
|
||||
}
|
||||
|
||||
private class SharedContactEventListener implements SharedContactView.EventListener {
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.conversation;
|
|||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.LinearLayout;
|
||||
|
@ -10,19 +9,27 @@ import android.widget.LinearLayout;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.components.Outliner;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public class ConversationItemBodyBubble extends LinearLayout {
|
||||
|
||||
@Nullable private List<Outliner> outliners = Collections.emptyList();
|
||||
@Nullable private OnSizeChangedListener sizeChangedListener;
|
||||
|
||||
private MaskDrawable maskDrawable;
|
||||
private Rect mask;
|
||||
private ClipProjectionDrawable clipProjectionDrawable;
|
||||
private Projection quoteViewProjection;
|
||||
private Projection videoPlayerProjection;
|
||||
|
||||
public ConversationItemBodyBubble(Context context) {
|
||||
super(context);
|
||||
|
@ -46,14 +53,26 @@ public class ConversationItemBodyBubble extends LinearLayout {
|
|||
|
||||
@Override
|
||||
public void setBackground(Drawable background) {
|
||||
maskDrawable = new MaskDrawable(background);
|
||||
maskDrawable.setMask(mask);
|
||||
super.setBackground(maskDrawable);
|
||||
clipProjectionDrawable = new ClipProjectionDrawable(background);
|
||||
|
||||
clipProjectionDrawable.setProjections(getProjections());
|
||||
super.setBackground(clipProjectionDrawable);
|
||||
}
|
||||
|
||||
public void setMask(@Nullable Rect mask) {
|
||||
this.mask = mask;
|
||||
maskDrawable.setMask(mask);
|
||||
public void setQuoteViewProjection(@Nullable Projection quoteViewProjection) {
|
||||
this.quoteViewProjection = quoteViewProjection;
|
||||
clipProjectionDrawable.setProjections(getProjections());
|
||||
}
|
||||
|
||||
public void setVideoPlayerProjection(@Nullable Projection videoPlayerProjection) {
|
||||
this.videoPlayerProjection = videoPlayerProjection;
|
||||
clipProjectionDrawable.setProjections(getProjections());
|
||||
}
|
||||
|
||||
public @NonNull Set<Projection> getProjections() {
|
||||
return Stream.of(quoteViewProjection, videoPlayerProjection)
|
||||
.filterNot(Objects::isNull)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -2,23 +2,30 @@ package org.thoughtcrime.securesms.conversation;
|
|||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.components.CornerMask;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.components.MaskView;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Masking area to ensure proper rendering of Reactions overlay.
|
||||
*/
|
||||
public final class ConversationItemMaskTarget extends MaskView.MaskTarget {
|
||||
|
||||
private final ConversationItem conversationItem;
|
||||
private final View videoContainer;
|
||||
private final Paint paint;
|
||||
|
||||
public ConversationItemMaskTarget(@NonNull ConversationItem conversationItem,
|
||||
@Nullable View videoContainer)
|
||||
|
@ -26,6 +33,10 @@ public final class ConversationItemMaskTarget extends MaskView.MaskTarget {
|
|||
super(conversationItem);
|
||||
this.conversationItem = conversationItem;
|
||||
this.videoContainer = videoContainer;
|
||||
this.paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
paint.setColor(Color.BLACK);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -41,19 +52,16 @@ public final class ConversationItemMaskTarget extends MaskView.MaskTarget {
|
|||
protected void draw(@NonNull Canvas canvas) {
|
||||
super.draw(canvas);
|
||||
|
||||
if (videoContainer == null) {
|
||||
return;
|
||||
List<Projection> projections = Stream.of(conversationItem.getColorizerProjections()).map(p ->
|
||||
Projection.translateFromRootToDescendantCoords(p, conversationItem)
|
||||
).toList();
|
||||
|
||||
if (videoContainer != null) {
|
||||
projections.add(conversationItem.getProjection((RecyclerView) conversationItem.getParent()));
|
||||
}
|
||||
|
||||
GiphyMp4Projection projection = conversationItem.getProjection((RecyclerView) conversationItem.getParent());
|
||||
CornerMask cornerMask = projection.getCornerMask();
|
||||
|
||||
canvas.clipRect(conversationItem.bodyBubble.getLeft(),
|
||||
conversationItem.bodyBubble.getTop(),
|
||||
conversationItem.bodyBubble.getRight(),
|
||||
conversationItem.bodyBubble.getTop() + projection.getHeight());
|
||||
|
||||
canvas.drawColor(Color.BLACK);
|
||||
cornerMask.mask(canvas);
|
||||
for (Projection projection : projections) {
|
||||
canvas.drawPath(projection.getPath(), paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
|||
import org.thoughtcrime.securesms.util.AccessibilityUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
||||
class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
public class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
||||
|
||||
private static float SWIPE_SUCCESS_DX = ConversationSwipeAnimationHelper.TRIGGER_DX;
|
||||
private static long SWIPE_SUCCESS_VIBE_TIME_MS = 10;
|
||||
|
@ -30,17 +30,17 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
|||
private final SwipeAvailabilityProvider swipeAvailabilityProvider;
|
||||
private final ConversationItemTouchListener itemTouchListener;
|
||||
private final OnSwipeListener onSwipeListener;
|
||||
private final GiphyMp4DisplayUpdater giphyMp4DisplayUpdater;
|
||||
private final OnViewHolderTranslated onViewHolderTranslated;
|
||||
|
||||
ConversationItemSwipeCallback(@NonNull SwipeAvailabilityProvider swipeAvailabilityProvider,
|
||||
@NonNull OnSwipeListener onSwipeListener,
|
||||
@NonNull GiphyMp4DisplayUpdater giphyMp4DisplayUpdater)
|
||||
@NonNull OnViewHolderTranslated onViewHolderTranslated)
|
||||
{
|
||||
super(0, ItemTouchHelper.END);
|
||||
this.itemTouchListener = new ConversationItemTouchListener(this::updateLatestDownCoordinate);
|
||||
this.swipeAvailabilityProvider = swipeAvailabilityProvider;
|
||||
this.onSwipeListener = onSwipeListener;
|
||||
this.giphyMp4DisplayUpdater = giphyMp4DisplayUpdater;
|
||||
this.onViewHolderTranslated = onViewHolderTranslated;
|
||||
this.shouldTriggerSwipeFeedback = true;
|
||||
this.canTriggerSwipe = true;
|
||||
}
|
||||
|
@ -93,14 +93,14 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
|||
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && isCorrectSwipeDir) {
|
||||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, Math.abs(dx), sign);
|
||||
updateVideoPlayer(recyclerView, viewHolder);
|
||||
dispatchTranslationUpdate(recyclerView, viewHolder);
|
||||
handleSwipeFeedback((ConversationItem) viewHolder.itemView, Math.abs(dx));
|
||||
if (canTriggerSwipe) {
|
||||
setTouchListener(recyclerView, viewHolder, Math.abs(dx));
|
||||
}
|
||||
} else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE || dx == 0) {
|
||||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView, 0, 1);
|
||||
updateVideoPlayer(recyclerView, viewHolder);
|
||||
dispatchTranslationUpdate(recyclerView, viewHolder);
|
||||
}
|
||||
|
||||
if (dx == 0) {
|
||||
|
@ -109,10 +109,8 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
|||
}
|
||||
}
|
||||
|
||||
private void updateVideoPlayer(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
if (viewHolder instanceof GiphyMp4Playable) {
|
||||
giphyMp4DisplayUpdater.updateDisplay(recyclerView, (GiphyMp4Playable) viewHolder);
|
||||
}
|
||||
private void dispatchTranslationUpdate(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
onViewHolderTranslated.onViewHolderTranslated(recyclerView, viewHolder);
|
||||
}
|
||||
|
||||
private void handleSwipeFeedback(@NonNull ConversationItem item, float dx) {
|
||||
|
@ -174,7 +172,7 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
|||
ConversationSwipeAnimationHelper.update((ConversationItem) viewHolder.itemView,
|
||||
0f,
|
||||
getSignFromDirection(viewHolder.itemView));
|
||||
updateVideoPlayer(recyclerView, viewHolder);
|
||||
dispatchTranslationUpdate(recyclerView, viewHolder);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,4 +209,8 @@ class ConversationItemSwipeCallback extends ItemTouchHelper.SimpleCallback {
|
|||
interface OnSwipeListener {
|
||||
void onSwipe(ConversationMessage conversationMessage);
|
||||
}
|
||||
|
||||
public interface OnViewHolderTranslated {
|
||||
void onViewHolderTranslated(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ import androidx.core.content.ContextCompat;
|
|||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
|
||||
|
@ -24,6 +23,7 @@ import org.signal.core.util.logging.Log;
|
|||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil;
|
||||
|
@ -31,12 +31,12 @@ import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
|||
import org.thoughtcrime.securesms.database.model.LiveUpdateMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
@ -47,6 +47,8 @@ import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
|
|||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
@ -109,7 +111,8 @@ public final class ConversationUpdateItem extends FrameLayout
|
|||
boolean hasWallpaper,
|
||||
boolean isMessageRequestAccepted,
|
||||
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
|
||||
boolean allowedToPlayInline)
|
||||
boolean allowedToPlayInline,
|
||||
@NonNull Colorizer colorizer)
|
||||
{
|
||||
this.batchSelected = batchSelected;
|
||||
|
||||
|
@ -206,7 +209,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
|||
}
|
||||
|
||||
@Override
|
||||
public @NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerView) {
|
||||
public @NonNull Projection getProjection(@NonNull ViewGroup recyclerView) {
|
||||
throw new UnsupportedOperationException("ConversationUpdateItems cannot be projected into.");
|
||||
}
|
||||
|
||||
|
@ -215,6 +218,11 @@ public final class ConversationUpdateItem extends FrameLayout
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<Projection> getColorizerProjections() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
static final class RecipientObserverManager {
|
||||
|
||||
private final Observer<Recipient> recipientObserver;
|
||||
|
|
|
@ -10,6 +10,8 @@ import androidx.lifecycle.Transformations;
|
|||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
@ -18,8 +20,13 @@ import org.signal.paging.PagedData;
|
|||
import org.signal.paging.PagingConfig;
|
||||
import org.signal.paging.PagingController;
|
||||
import org.signal.paging.ProxyPagingController;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.conversation.colors.NameColor;
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaRepository;
|
||||
import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent;
|
||||
|
@ -30,8 +37,11 @@ import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
|||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ConversationViewModel extends ViewModel {
|
||||
|
||||
|
@ -52,6 +62,7 @@ public class ConversationViewModel extends ViewModel {
|
|||
private final MutableLiveData<RecipientId> recipientId;
|
||||
private final LiveData<ChatWallpaper> wallpaper;
|
||||
private final SingleLiveEvent<Event> events;
|
||||
private final LiveData<ChatColors> chatColors;
|
||||
|
||||
private ConversationIntents.Args args;
|
||||
private int jumpToPosition;
|
||||
|
@ -114,11 +125,15 @@ public class ConversationViewModel extends ViewModel {
|
|||
|
||||
conversationMetadata = Transformations.switchMap(messages, m -> metadata);
|
||||
canShowAsBubble = LiveDataUtil.mapAsync(threadId, conversationRepository::canShowAsBubble);
|
||||
wallpaper = Transformations.distinctUntilChanged(Transformations.map(Transformations.switchMap(recipientId,
|
||||
id -> Recipient.live(id).getLiveData()),
|
||||
Recipient::getWallpaper));
|
||||
wallpaper = LiveDataUtil.mapDistinct(Transformations.switchMap(recipientId,
|
||||
id -> Recipient.live(id).getLiveData()),
|
||||
Recipient::getWallpaper);
|
||||
|
||||
EventBus.getDefault().register(this);
|
||||
|
||||
chatColors = LiveDataUtil.mapDistinct(Transformations.switchMap(recipientId,
|
||||
id -> Recipient.live(id).getLiveData()),
|
||||
Recipient::getChatColors);
|
||||
}
|
||||
|
||||
void onAttachmentKeyboardOpen() {
|
||||
|
@ -159,6 +174,10 @@ public class ConversationViewModel extends ViewModel {
|
|||
return events;
|
||||
}
|
||||
|
||||
@NonNull LiveData<ChatColors> getChatColors() {
|
||||
return chatColors;
|
||||
}
|
||||
|
||||
void setHasUnreadMentions(boolean hasUnreadMentions) {
|
||||
this.hasUnreadMentions.setValue(hasUnreadMentions);
|
||||
}
|
||||
|
@ -183,6 +202,35 @@ public class ConversationViewModel extends ViewModel {
|
|||
return pagingController;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Map<RecipientId, NameColor>> getNameColorsMap() {
|
||||
LiveData<Recipient> recipient = Transformations.switchMap(recipientId, r -> Recipient.live(r).getLiveData());
|
||||
LiveData<Recipient> groupRecipients = LiveDataUtil.filter(recipient, Recipient::isGroup);
|
||||
LiveData<List<GroupMemberEntry.FullMember>> groupMembers = Transformations.switchMap(groupRecipients, r -> new LiveGroup(r.getGroupId().get()).getFullMembers());
|
||||
|
||||
return Transformations.map(groupMembers, members -> {
|
||||
List<GroupMemberEntry.FullMember> sorted = Stream.of(members)
|
||||
.filter(member -> !Objects.equals(member.getMember(), Recipient.self()))
|
||||
.sortBy(this::getMemberIdentifier)
|
||||
.toList();
|
||||
|
||||
List<NameColor> names = ChatColorsPalette.Names.getAll();
|
||||
Map<RecipientId, NameColor> colors = new HashMap<>();
|
||||
for (int i = 0; i < sorted.size(); i++) {
|
||||
colors.put(sorted.get(i).getMember().getId(), names.get(i % names.size()));
|
||||
}
|
||||
|
||||
return colors;
|
||||
});
|
||||
}
|
||||
|
||||
private @NonNull String getMemberIdentifier(@NonNull GroupMemberEntry.FullMember fullMember) {
|
||||
return fullMember.getMember()
|
||||
.getUuid()
|
||||
.transform(UUID::toString)
|
||||
.or(fullMember.getMember().getE164())
|
||||
.or("");
|
||||
}
|
||||
|
||||
long getLastSeen() {
|
||||
return conversationMetadata.getValue() != null ? conversationMetadata.getValue().getLastSeen() : 0;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import androidx.annotation.Nullable;
|
|||
|
||||
/**
|
||||
* Drawable which lets you punch a hole through another drawable.
|
||||
*
|
||||
* TODO: Remove in favor of ClipProjectionDrawable
|
||||
*/
|
||||
public final class MaskDrawable extends Drawable {
|
||||
|
||||
|
|
|
@ -0,0 +1,226 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors
|
||||
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Path
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import com.google.common.base.Objects
|
||||
import org.thoughtcrime.securesms.components.RotatableGradientDrawable
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor
|
||||
import org.thoughtcrime.securesms.util.customizeOnDraw
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* ChatColors represent how to render the avatar and bubbles in a given context.
|
||||
*
|
||||
* @param id The identifier for this chat color. It is either BuiltIn, NotSet, or Custom(long)
|
||||
* @param linearGradient The LinearGradient to render. Null if this is for a single color.
|
||||
* @param singleColor The single color to render. Null if this is for a linear gradient.
|
||||
*/
|
||||
class ChatColors private constructor(
|
||||
val id: Id,
|
||||
private val linearGradient: LinearGradient?,
|
||||
private val singleColor: Int?
|
||||
) {
|
||||
|
||||
/**
|
||||
* Returns the Drawable to render the linear gradient, or null if this ChatColors is a single color.
|
||||
*/
|
||||
val chatBubbleMask: Drawable
|
||||
get() = linearGradient?.let {
|
||||
RotatableGradientDrawable(
|
||||
it.degrees,
|
||||
it.colors,
|
||||
it.positions
|
||||
)
|
||||
} ?: ColorDrawable(asSingleColor())
|
||||
|
||||
/**
|
||||
* Returns the ColorFilter to apply to a conversation bubble or other relevant piece of UI.
|
||||
*/
|
||||
val chatBubbleColorFilter: ColorFilter = PorterDuffColorFilter(Color.TRANSPARENT, PorterDuff.Mode.SRC_IN)
|
||||
|
||||
@ColorInt
|
||||
fun asSingleColor(): Int {
|
||||
if (singleColor != null) {
|
||||
return singleColor
|
||||
}
|
||||
|
||||
if (linearGradient != null) {
|
||||
val start = linearGradient.colors.first()
|
||||
val end = linearGradient.colors.last()
|
||||
|
||||
return ColorUtils.blendARGB(start, end, 0.5f)
|
||||
}
|
||||
|
||||
throw AssertionError()
|
||||
}
|
||||
|
||||
fun serialize(): ChatColor {
|
||||
val builder: ChatColor.Builder = ChatColor.newBuilder()
|
||||
|
||||
if (linearGradient != null) {
|
||||
val gradientBuilder = ChatColor.LinearGradient.newBuilder()
|
||||
|
||||
gradientBuilder.rotation = linearGradient.degrees
|
||||
linearGradient.colors.forEach { gradientBuilder.addColors(it) }
|
||||
linearGradient.positions.forEach { gradientBuilder.addPositions(it) }
|
||||
|
||||
builder.setLinearGradient(gradientBuilder)
|
||||
}
|
||||
|
||||
if (singleColor != null) {
|
||||
builder.setSingleColor(ChatColor.SingleColor.newBuilder().setColor(singleColor))
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun getColors(): IntArray {
|
||||
return linearGradient?.colors ?: if (singleColor != null) {
|
||||
intArrayOf(singleColor)
|
||||
} else {
|
||||
throw AssertionError()
|
||||
}
|
||||
}
|
||||
|
||||
fun getDegrees(): Float {
|
||||
return linearGradient?.degrees ?: 180f
|
||||
}
|
||||
|
||||
fun asCircle(): Drawable {
|
||||
val toWrap: Drawable = chatBubbleMask
|
||||
val path = Path()
|
||||
|
||||
return toWrap.customizeOnDraw { wrapped, canvas ->
|
||||
canvas.save()
|
||||
path.rewind()
|
||||
path.addCircle(
|
||||
wrapped.bounds.exactCenterX(),
|
||||
wrapped.bounds.exactCenterY(),
|
||||
min(wrapped.bounds.exactCenterX(), wrapped.bounds.exactCenterY()),
|
||||
Path.Direction.CW
|
||||
)
|
||||
canvas.clipPath(path)
|
||||
wrapped.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
|
||||
fun withId(id: Id): ChatColors = ChatColors(id, linearGradient, singleColor)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
val otherChatColors: ChatColors = (other as? ChatColors) ?: return false
|
||||
|
||||
if (id != otherChatColors.id) return false
|
||||
if (linearGradient != otherChatColors.linearGradient) return false
|
||||
if (singleColor != otherChatColors.singleColor) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Objects.hashCode(linearGradient, singleColor, id)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun forChatColor(id: Id, chatColor: ChatColor): ChatColors {
|
||||
assert(chatColor.hasSingleColor() xor chatColor.hasLinearGradient())
|
||||
|
||||
return if (chatColor.hasLinearGradient()) {
|
||||
val linearGradient = LinearGradient(
|
||||
chatColor.linearGradient.rotation,
|
||||
chatColor.linearGradient.colorsList.toIntArray(),
|
||||
chatColor.linearGradient.positionsList.toFloatArray()
|
||||
)
|
||||
|
||||
forGradient(id, linearGradient)
|
||||
} else {
|
||||
val singleColor = chatColor.singleColor.color
|
||||
|
||||
forColor(id, singleColor)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun forGradient(id: Id, linearGradient: LinearGradient): ChatColors =
|
||||
ChatColors(id, linearGradient, null)
|
||||
|
||||
@JvmStatic
|
||||
fun forColor(id: Id, @ColorInt color: Int): ChatColors =
|
||||
ChatColors(id, null, color)
|
||||
}
|
||||
|
||||
sealed class Id(val longValue: Long) {
|
||||
/**
|
||||
* Represents user selection of 'auto'.
|
||||
*/
|
||||
object Auto : Id(-2)
|
||||
|
||||
/**
|
||||
* Represents a built in color.
|
||||
*/
|
||||
object BuiltIn : Id(-1)
|
||||
|
||||
/**
|
||||
* Represents an unsaved or un-set option.
|
||||
*/
|
||||
object NotSet : Id(0)
|
||||
|
||||
/**
|
||||
* Represents a custom created ChatColors.
|
||||
*/
|
||||
class Custom internal constructor(id: Long) : Id(id)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return longValue == (other as? Id)?.longValue
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return Objects.hashCode(longValue)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun forLongValue(longValue: Long): Id {
|
||||
return when (longValue) {
|
||||
-2L -> Auto
|
||||
-1L -> BuiltIn
|
||||
0L -> NotSet
|
||||
else -> Custom(longValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class LinearGradient(
|
||||
val degrees: Float,
|
||||
val colors: IntArray,
|
||||
val positions: FloatArray
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as LinearGradient
|
||||
|
||||
if (!colors.contentEquals(other.colors)) return false
|
||||
if (!positions.contentEquals(other.positions)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = colors.contentHashCode()
|
||||
result = 31 * result + positions.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors
|
||||
|
||||
import com.google.common.collect.BiMap
|
||||
import com.google.common.collect.ImmutableBiMap
|
||||
import com.google.common.collect.ImmutableMap
|
||||
import org.thoughtcrime.securesms.color.MaterialColor
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
import org.thoughtcrime.securesms.wallpaper.GradientChatWallpaper
|
||||
import org.thoughtcrime.securesms.wallpaper.SingleColorChatWallpaper
|
||||
|
||||
/**
|
||||
* Contains mappings to get the relevant chat colors for either a legacy MaterialColor or a built-in wallpaper.
|
||||
*/
|
||||
object ChatColorsMapper {
|
||||
|
||||
private val materialColorToChatColorsBiMap: BiMap<MaterialColor, ChatColors> = ImmutableBiMap.Builder<MaterialColor, ChatColors>().apply {
|
||||
put(MaterialColor.CRIMSON, ChatColorsPalette.Bubbles.CRIMSON)
|
||||
put(MaterialColor.VERMILLION, ChatColorsPalette.Bubbles.VERMILION)
|
||||
put(MaterialColor.BURLAP, ChatColorsPalette.Bubbles.BURLAP)
|
||||
put(MaterialColor.FOREST, ChatColorsPalette.Bubbles.FOREST)
|
||||
put(MaterialColor.WINTERGREEN, ChatColorsPalette.Bubbles.WINTERGREEN)
|
||||
put(MaterialColor.TEAL, ChatColorsPalette.Bubbles.TEAL)
|
||||
put(MaterialColor.BLUE, ChatColorsPalette.Bubbles.BLUE)
|
||||
put(MaterialColor.INDIGO, ChatColorsPalette.Bubbles.INDIGO)
|
||||
put(MaterialColor.VIOLET, ChatColorsPalette.Bubbles.VIOLET)
|
||||
put(MaterialColor.PLUM, ChatColorsPalette.Bubbles.PLUM)
|
||||
put(MaterialColor.TAUPE, ChatColorsPalette.Bubbles.TAUPE)
|
||||
put(MaterialColor.STEEL, ChatColorsPalette.Bubbles.STEEL)
|
||||
put(MaterialColor.ULTRAMARINE, ChatColorsPalette.Bubbles.ULTRAMARINE)
|
||||
}.build()
|
||||
|
||||
private val wallpaperToChatColorsMap: Map<ChatWallpaper, ChatColors> = ImmutableMap.Builder<ChatWallpaper, ChatColors>().apply {
|
||||
put(SingleColorChatWallpaper.BLUSH, ChatColorsPalette.Bubbles.CRIMSON)
|
||||
put(SingleColorChatWallpaper.COPPER, ChatColorsPalette.Bubbles.VERMILION)
|
||||
put(SingleColorChatWallpaper.DUST, ChatColorsPalette.Bubbles.BURLAP)
|
||||
put(SingleColorChatWallpaper.CELADON, ChatColorsPalette.Bubbles.FOREST)
|
||||
put(SingleColorChatWallpaper.RAINFOREST, ChatColorsPalette.Bubbles.WINTERGREEN)
|
||||
put(SingleColorChatWallpaper.PACIFIC, ChatColorsPalette.Bubbles.TEAL)
|
||||
put(SingleColorChatWallpaper.FROST, ChatColorsPalette.Bubbles.BLUE)
|
||||
put(SingleColorChatWallpaper.NAVY, ChatColorsPalette.Bubbles.INDIGO)
|
||||
put(SingleColorChatWallpaper.LILAC, ChatColorsPalette.Bubbles.VIOLET)
|
||||
put(SingleColorChatWallpaper.PINK, ChatColorsPalette.Bubbles.PLUM)
|
||||
put(SingleColorChatWallpaper.EGGPLANT, ChatColorsPalette.Bubbles.TAUPE)
|
||||
put(SingleColorChatWallpaper.SILVER, ChatColorsPalette.Bubbles.STEEL)
|
||||
put(GradientChatWallpaper.SUNSET, ChatColorsPalette.Bubbles.EMBER)
|
||||
put(GradientChatWallpaper.NOIR, ChatColorsPalette.Bubbles.MIDNIGHT)
|
||||
put(GradientChatWallpaper.HEATMAP, ChatColorsPalette.Bubbles.INFRARED)
|
||||
put(GradientChatWallpaper.AQUA, ChatColorsPalette.Bubbles.LAGOON)
|
||||
put(GradientChatWallpaper.IRIDESCENT, ChatColorsPalette.Bubbles.FLUORESCENT)
|
||||
put(GradientChatWallpaper.MONSTERA, ChatColorsPalette.Bubbles.BASIL)
|
||||
put(GradientChatWallpaper.BLISS, ChatColorsPalette.Bubbles.SUBLIME)
|
||||
put(GradientChatWallpaper.SKY, ChatColorsPalette.Bubbles.SEA)
|
||||
put(GradientChatWallpaper.PEACH, ChatColorsPalette.Bubbles.TANGERINE)
|
||||
}.build()
|
||||
|
||||
@JvmStatic
|
||||
val entrySet: Set<MutableMap.MutableEntry<MaterialColor, ChatColors>>
|
||||
get() = materialColorToChatColorsBiMap.entries
|
||||
|
||||
@JvmStatic
|
||||
fun getChatColors(materialColor: MaterialColor): ChatColors {
|
||||
return materialColorToChatColorsBiMap[materialColor] ?: ChatColorsPalette.Bubbles.default
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getChatColors(wallpaper: ChatWallpaper): ChatColors {
|
||||
return wallpaperToChatColorsMap.entries.find { (key, _) ->
|
||||
key.isSameSource(wallpaper)
|
||||
}?.value ?: ChatColorsPalette.Bubbles.default
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getMaterialColor(chatColors: ChatColors): MaterialColor {
|
||||
return materialColorToChatColorsBiMap.inverse()[chatColors] ?: MaterialColor.ULTRAMARINE
|
||||
}
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors
|
||||
|
||||
/**
|
||||
* Namespaced collection of supported bubble colors and name colors.
|
||||
*/
|
||||
object ChatColorsPalette {
|
||||
object Bubbles {
|
||||
|
||||
// region Default
|
||||
|
||||
@JvmField
|
||||
val ULTRAMARINE = ChatColors.forGradient(
|
||||
ChatColors.Id.BuiltIn,
|
||||
ChatColors.LinearGradient(
|
||||
180.0f,
|
||||
intArrayOf(0xFF0552F0.toInt(), 0xFF2C6BED.toInt()),
|
||||
floatArrayOf(0f, 1f)
|
||||
)
|
||||
)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Solids
|
||||
|
||||
@JvmField
|
||||
val CRIMSON = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFFCF16E3.toInt())
|
||||
|
||||
@JvmField
|
||||
val VERMILION = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFFC73F0A.toInt())
|
||||
|
||||
@JvmField
|
||||
val BURLAP = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF6F6A58.toInt())
|
||||
|
||||
@JvmField
|
||||
val FOREST = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF3B7845.toInt())
|
||||
|
||||
@JvmField
|
||||
val WINTERGREEN = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF1D8663.toInt())
|
||||
|
||||
@JvmField
|
||||
val TEAL = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF077D92.toInt())
|
||||
|
||||
@JvmField
|
||||
val BLUE = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF336BA3.toInt())
|
||||
|
||||
@JvmField
|
||||
val INDIGO = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF6058CA.toInt())
|
||||
|
||||
@JvmField
|
||||
val VIOLET = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFF9932CB.toInt())
|
||||
|
||||
@JvmField
|
||||
val PLUM = ChatColors.forColor(ChatColors.Id.BuiltIn, 0xFFAA377A.toInt())
|
||||
|
||||
@JvmField
|
||||
val TAUPE = ChatColors.forColor(
|
||||
ChatColors.Id.BuiltIn, 0xFF8F616A.toInt()
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val STEEL = ChatColors.forColor(
|
||||
ChatColors.Id.BuiltIn, 0xFF71717F.toInt()
|
||||
)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Gradients
|
||||
|
||||
@JvmField
|
||||
val EMBER = ChatColors.forGradient(
|
||||
ChatColors.Id.BuiltIn,
|
||||
ChatColors.LinearGradient(
|
||||
168f,
|
||||
intArrayOf(0xFFE57C00.toInt(), 0xFF5E0000.toInt()),
|
||||
floatArrayOf(0f, 1f)
|
||||
)
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val MIDNIGHT = ChatColors.forGradient(
|
||||
ChatColors.Id.BuiltIn,
|
||||
ChatColors.LinearGradient(
|
||||
180f,
|
||||
intArrayOf(0xFF2C2C3A.toInt(), 0xFF787891.toInt()),
|
||||
floatArrayOf(0f, 1f)
|
||||
)
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val INFRARED = ChatColors.forGradient(
|
||||
ChatColors.Id.BuiltIn,
|
||||
ChatColors.LinearGradient(
|
||||
192f,
|
||||
intArrayOf(0xFFF65560.toInt(), 0xFF442CED.toInt()),
|
||||
floatArrayOf(0f, 1f)
|
||||
)
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val LAGOON = ChatColors.forGradient(
|
||||
ChatColors.Id.BuiltIn,
|
||||
ChatColors.LinearGradient(
|
||||
180f,
|
||||
intArrayOf(0xFF004066.toInt(), 0xFF32867D.toInt()),
|
||||
floatArrayOf(0f, 1f)
|
||||
)
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val FLUORESCENT = ChatColors.forGradient(
|
||||
ChatColors.Id.BuiltIn,
|
||||
ChatColors.LinearGradient(
|
||||
192f,
|
||||
intArrayOf(0xFFEC13DD.toInt(), 0xFF1B36C6.toInt()),
|
||||
floatArrayOf(0f, 1f)
|
||||
)
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val BASIL = ChatColors.forGradient(
|
||||
ChatColors.Id.BuiltIn,
|
||||
ChatColors.LinearGradient(
|
||||
180f,
|
||||
intArrayOf(0xFF2F9373.toInt(), 0xFF077343.toInt()),
|
||||
floatArrayOf(0f, 1f)
|
||||
)
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val SUBLIME = ChatColors.forGradient(
|
||||
ChatColors.Id.BuiltIn,
|
||||
ChatColors.LinearGradient(
|
||||
180f,
|
||||
intArrayOf(
|
||||
0xFF6281D5.toInt(), 0xFF974460.toInt()
|
||||
),
|
||||
floatArrayOf(0f, 1f)
|
||||
)
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val SEA = ChatColors.forGradient(
|
||||
ChatColors.Id.BuiltIn,
|
||||
ChatColors.LinearGradient(
|
||||
180f,
|
||||
intArrayOf(0xFF498FD4.toInt(), 0xFF2C66A0.toInt()),
|
||||
floatArrayOf(0f, 1f)
|
||||
)
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val TANGERINE = ChatColors.forGradient(
|
||||
ChatColors.Id.BuiltIn,
|
||||
ChatColors.LinearGradient(
|
||||
192f,
|
||||
intArrayOf(0xFFDB7133.toInt(), 0xFF911231.toInt()),
|
||||
floatArrayOf(0f, 1f)
|
||||
)
|
||||
)
|
||||
|
||||
// endregion
|
||||
|
||||
@JvmStatic
|
||||
val default = ULTRAMARINE
|
||||
|
||||
val solids = listOf(
|
||||
CRIMSON,
|
||||
VERMILION,
|
||||
BURLAP,
|
||||
FOREST,
|
||||
WINTERGREEN,
|
||||
TEAL,
|
||||
BLUE,
|
||||
INDIGO,
|
||||
VIOLET,
|
||||
PLUM,
|
||||
TAUPE,
|
||||
STEEL
|
||||
)
|
||||
val gradients =
|
||||
listOf(EMBER, MIDNIGHT, INFRARED, LAGOON, FLUORESCENT, BASIL, SUBLIME, SEA, TANGERINE)
|
||||
val all = listOf(default) + solids + gradients
|
||||
}
|
||||
|
||||
object Names {
|
||||
@JvmStatic
|
||||
val all = listOf(
|
||||
NameColor(lightColor = 0xFFD00B0B.toInt(), darkColor = 0xFFF76E6E.toInt()),
|
||||
NameColor(lightColor = 0xFF067906.toInt(), darkColor = 0xFF0AB80A.toInt()),
|
||||
NameColor(lightColor = 0xFF5151F6.toInt(), darkColor = 0xFF8B8BF9.toInt()),
|
||||
NameColor(lightColor = 0xFF866118.toInt(), darkColor = 0xFFD08F0B.toInt()),
|
||||
NameColor(lightColor = 0xFF067953.toInt(), darkColor = 0xFF09B37B.toInt()),
|
||||
NameColor(lightColor = 0xFFA20CED.toInt(), darkColor = 0xFFCB72F8.toInt()),
|
||||
NameColor(lightColor = 0xFF507406.toInt(), darkColor = 0xFF77AE09.toInt()),
|
||||
NameColor(lightColor = 0xFF086DA0.toInt(), darkColor = 0xFF0DA6F2.toInt()),
|
||||
NameColor(lightColor = 0xFFC70A88.toInt(), darkColor = 0xFFF76EC9.toInt()),
|
||||
NameColor(lightColor = 0xFFB34209.toInt(), darkColor = 0xFFF4702F.toInt()),
|
||||
NameColor(lightColor = 0xFF06792D.toInt(), darkColor = 0xFF0AB844.toInt()),
|
||||
NameColor(lightColor = 0xFF7A3DF5.toInt(), darkColor = 0xFFAC86F9.toInt()),
|
||||
NameColor(lightColor = 0xFF6C6C13.toInt(), darkColor = 0xFFA5A509.toInt()),
|
||||
NameColor(lightColor = 0xFF067474.toInt(), darkColor = 0xFF09AEAE.toInt()),
|
||||
NameColor(lightColor = 0xFFB80AB8.toInt(), darkColor = 0xFFF75FF7.toInt()),
|
||||
NameColor(lightColor = 0xFF2D7906.toInt(), darkColor = 0xFF42B309.toInt()),
|
||||
NameColor(lightColor = 0xFF0D59F2.toInt(), darkColor = 0xFF6495F7.toInt()),
|
||||
NameColor(lightColor = 0xFFD00B4D.toInt(), darkColor = 0xFFF76998.toInt()),
|
||||
NameColor(lightColor = 0xFFC72A0A.toInt(), darkColor = 0xFFF67055.toInt()),
|
||||
NameColor(lightColor = 0xFF067919.toInt(), darkColor = 0xFF0AB827.toInt()),
|
||||
NameColor(lightColor = 0xFF6447F5.toInt(), darkColor = 0xFF9986F9.toInt()),
|
||||
NameColor(lightColor = 0xFF76681E.toInt(), darkColor = 0xFFB89B0A.toInt()),
|
||||
NameColor(lightColor = 0xFF067462.toInt(), darkColor = 0xFF09B397.toInt()),
|
||||
NameColor(lightColor = 0xFFAF0BD0.toInt(), darkColor = 0xFFE06EF7.toInt()),
|
||||
NameColor(lightColor = 0xFF3D7406.toInt(), darkColor = 0xFF5EB309.toInt()),
|
||||
NameColor(lightColor = 0xFF0A69C7.toInt(), darkColor = 0xFF429CF5.toInt()),
|
||||
NameColor(lightColor = 0xFFCB0B6B.toInt(), darkColor = 0xFFF76EB2.toInt()),
|
||||
NameColor(lightColor = 0xFF9C5711.toInt(), darkColor = 0xFFE97A0C.toInt()),
|
||||
NameColor(lightColor = 0xFF067940.toInt(), darkColor = 0xFF09B35E.toInt()),
|
||||
NameColor(lightColor = 0xFF8F2AF4.toInt(), darkColor = 0xFFBD81F8.toInt()),
|
||||
NameColor(lightColor = 0xFF5E6E0C.toInt(), darkColor = 0xFF8FAA09.toInt()),
|
||||
NameColor(lightColor = 0xFF077288.toInt(), darkColor = 0xFF0BABCB.toInt()),
|
||||
NameColor(lightColor = 0xFFC20AA3.toInt(), darkColor = 0xFFF75FDD.toInt()),
|
||||
NameColor(lightColor = 0xFF1A7906.toInt(), darkColor = 0xFF27B80A.toInt()),
|
||||
NameColor(lightColor = 0xFF3454F4.toInt(), darkColor = 0xFF778DF8.toInt()),
|
||||
NameColor(lightColor = 0xFFD00B2C.toInt(), darkColor = 0xFFF76E85.toInt())
|
||||
)
|
||||
}
|
||||
|
||||
@JvmField
|
||||
val UNKNOWN_CONTACT = Bubbles.STEEL
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors
|
||||
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
|
||||
/**
|
||||
* Denotes that a class can be colorized. The class is responsible for
|
||||
* generating its own projection.
|
||||
*/
|
||||
interface Colorizable {
|
||||
val colorizerProjections: List<Projection>
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
|
||||
/**
|
||||
* Helper class for all things ChatColors.
|
||||
*
|
||||
* - Maintains a mapping for group recipient colors
|
||||
* - Gives easy access to different bubble colors
|
||||
* - Watches and responds to RecyclerView scroll and layout changes to update a ColorizerView
|
||||
*/
|
||||
class Colorizer(private val colorizerView: ColorizerView) : RecyclerView.OnScrollListener(), View.OnLayoutChangeListener {
|
||||
|
||||
private val groupSenderColors: MutableMap<RecipientId, NameColor> = mutableMapOf()
|
||||
|
||||
@ColorInt
|
||||
fun getOutgoingBodyTextColor(context: Context): Int {
|
||||
return ContextCompat.getColor(context, R.color.white)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun getOutgoingFooterTextColor(context: Context): Int {
|
||||
return ContextCompat.getColor(context, R.color.conversation_item_outgoing_footer_fg)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun getOutgoingFooterIconColor(context: Context): Int {
|
||||
return ContextCompat.getColor(context, R.color.conversation_item_outgoing_footer_fg)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun getIncomingGroupSenderColor(context: Context, recipient: Recipient): Int = groupSenderColors[recipient.id]?.getColor(context) ?: Color.TRANSPARENT
|
||||
|
||||
fun attachToRecyclerView(recyclerView: RecyclerView) {
|
||||
recyclerView.addOnScrollListener(this)
|
||||
recyclerView.addOnLayoutChangeListener(this)
|
||||
}
|
||||
|
||||
fun onNameColorsChanged(nameColorMap: Map<RecipientId, NameColor>) {
|
||||
groupSenderColors.clear()
|
||||
groupSenderColors.putAll(nameColorMap)
|
||||
}
|
||||
|
||||
fun onChatColorsChanged(chatColors: ChatColors) {
|
||||
colorizerView.background = chatColors.chatBubbleMask
|
||||
}
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
applyClipPathsToMaskedGradient(recyclerView)
|
||||
}
|
||||
|
||||
override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
||||
applyClipPathsToMaskedGradient(v as RecyclerView)
|
||||
}
|
||||
|
||||
fun applyClipPathsToMaskedGradient(recyclerView: RecyclerView) {
|
||||
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
|
||||
|
||||
val firstVisibleItemPosition: Int = layoutManager.findFirstVisibleItemPosition()
|
||||
val lastVisibleItemPosition: Int = layoutManager.findLastVisibleItemPosition()
|
||||
|
||||
val projections: List<Projection> = (firstVisibleItemPosition..lastVisibleItemPosition)
|
||||
.mapNotNull { recyclerView.findViewHolderForAdapterPosition(it) as? Colorizable }
|
||||
.map {
|
||||
it.colorizerProjections
|
||||
.map { p -> Projection.translateFromRootToDescendantCoords(p, colorizerView) }
|
||||
}
|
||||
.flatten()
|
||||
|
||||
if (projections.isNotEmpty()) {
|
||||
colorizerView.visibility = View.VISIBLE
|
||||
colorizerView.setProjections(projections)
|
||||
} else {
|
||||
colorizerView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Path
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
|
||||
/**
|
||||
* ColorizerView takes a list of projections and uses them to create a mask over it's background.
|
||||
*/
|
||||
class ColorizerView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val clipPath = Path()
|
||||
private var projections: List<Projection> = listOf()
|
||||
|
||||
fun setProjections(projections: List<Projection>) {
|
||||
this.projections = projections
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
if (projections.isNotEmpty()) {
|
||||
canvas.save()
|
||||
clipPath.rewind()
|
||||
|
||||
projections.forEach {
|
||||
it.applyToPath(clipPath)
|
||||
}
|
||||
|
||||
canvas.clipPath(clipPath)
|
||||
super.draw(canvas)
|
||||
canvas.restore()
|
||||
} else {
|
||||
super.draw(canvas)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.ColorInt
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
|
||||
/**
|
||||
* Class which stores information for a Recipient's name color in a group.
|
||||
*/
|
||||
class NameColor(
|
||||
@ColorInt private val lightColor: Int,
|
||||
@ColorInt private val darkColor: Int
|
||||
) {
|
||||
@ColorInt
|
||||
fun getColor(context: Context): Int {
|
||||
return if (ThemeUtil.isDarkTheme(context)) {
|
||||
darkColor
|
||||
} else {
|
||||
lightColor
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
|
||||
class ChatColorMappingModel(
|
||||
val chatColors: ChatColors,
|
||||
val isSelected: Boolean,
|
||||
val isAuto: Boolean
|
||||
) : MappingModel<ChatColorMappingModel> {
|
||||
|
||||
val isCustom: Boolean = chatColors.id is ChatColors.Id.Custom
|
||||
|
||||
override fun areItemsTheSame(newItem: ChatColorMappingModel): Boolean {
|
||||
return chatColors == newItem.chatColors && isAuto == newItem.isAuto
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: ChatColorMappingModel): Boolean {
|
||||
return areItemsTheSame(newItem) && isSelected == newItem.isSelected
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.TypedArray
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.DeliveryStatusView
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.colors.ColorizerView
|
||||
import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.Projection
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
import java.util.Locale
|
||||
|
||||
private val TAG = Log.tag(ChatColorPreviewView::class.java)
|
||||
|
||||
class ChatColorPreviewView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val wallpaper: ImageView
|
||||
private val wallpaperDim: View
|
||||
private val colorizerView: ColorizerView
|
||||
|
||||
private val recv1: Bubble
|
||||
private val sent1: Bubble
|
||||
private val recv2: Bubble
|
||||
private val sent2: Bubble
|
||||
|
||||
private val bubbleCount: Int
|
||||
private val colorizer: Colorizer
|
||||
|
||||
private var chatColors: ChatColors? = null
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.chat_colors_preview_view, this)
|
||||
|
||||
var typedArray: TypedArray? = null
|
||||
try {
|
||||
typedArray = context.obtainStyledAttributes(attrs, R.styleable.ChatColorPreviewView, 0, 0)
|
||||
|
||||
bubbleCount = typedArray.getInteger(R.styleable.ChatColorPreviewView_ccpv_chat_bubble_count, 2)
|
||||
|
||||
assert(bubbleCount == 2 || bubbleCount == 4) {
|
||||
Log.e(TAG, "Bubble count must be 2 or 4")
|
||||
}
|
||||
|
||||
recv1 = Bubble(
|
||||
findViewById(R.id.bubble_1),
|
||||
findViewById(R.id.bubble_1_text),
|
||||
findViewById(R.id.bubble_1_time),
|
||||
null
|
||||
)
|
||||
|
||||
sent1 = Bubble(
|
||||
findViewById(R.id.bubble_2),
|
||||
findViewById(R.id.bubble_2_text),
|
||||
findViewById(R.id.bubble_2_time),
|
||||
findViewById(R.id.bubble_2_delivery)
|
||||
)
|
||||
|
||||
recv2 = Bubble(
|
||||
findViewById(R.id.bubble_3),
|
||||
findViewById(R.id.bubble_3_text),
|
||||
findViewById(R.id.bubble_3_time),
|
||||
null
|
||||
)
|
||||
|
||||
sent2 = Bubble(
|
||||
findViewById(R.id.bubble_4),
|
||||
findViewById(R.id.bubble_4_text),
|
||||
findViewById(R.id.bubble_4_time),
|
||||
findViewById(R.id.bubble_4_delivery)
|
||||
)
|
||||
|
||||
val now: String = DateUtils.getExtendedRelativeTimeSpanString(context, Locale.getDefault(), System.currentTimeMillis())
|
||||
listOf(sent1, sent2, recv1, recv2).forEach {
|
||||
it.time.text = now
|
||||
it.delivery?.setRead()
|
||||
}
|
||||
|
||||
if (bubbleCount == 2) {
|
||||
recv2.bubble.visibility = View.GONE
|
||||
sent2.bubble.visibility = View.GONE
|
||||
}
|
||||
|
||||
wallpaper = findViewById(R.id.wallpaper)
|
||||
wallpaperDim = findViewById(R.id.wallpaper_dim)
|
||||
colorizerView = findViewById(R.id.colorizer)
|
||||
colorizer = Colorizer(colorizerView)
|
||||
} finally {
|
||||
typedArray?.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
if (chatColors != null) {
|
||||
setChatColors(requireNotNull(chatColors))
|
||||
}
|
||||
}
|
||||
|
||||
fun setWallpaper(chatWallpaper: ChatWallpaper?) {
|
||||
if (chatWallpaper != null) {
|
||||
chatWallpaper.loadInto(wallpaper)
|
||||
|
||||
if (ThemeUtil.isDarkTheme(context)) {
|
||||
wallpaperDim.alpha = chatWallpaper.dimLevelForDarkTheme
|
||||
} else {
|
||||
wallpaperDim.alpha = 0f
|
||||
}
|
||||
} else {
|
||||
wallpaper.background = null
|
||||
wallpaperDim.alpha = 0f
|
||||
}
|
||||
|
||||
val backgroundColor = if (chatWallpaper != null) {
|
||||
R.color.conversation_item_wallpaper_bubble_color
|
||||
} else {
|
||||
R.color.signal_background_secondary
|
||||
}
|
||||
|
||||
listOf(recv1, recv2).forEach {
|
||||
it.bubble.background.colorFilter = PorterDuffColorFilter(
|
||||
ContextCompat.getColor(context, backgroundColor),
|
||||
PorterDuff.Mode.SRC_IN
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setChatColors(chatColors: ChatColors) {
|
||||
this.chatColors = chatColors
|
||||
|
||||
val sentBubbles = listOf(sent1, sent2)
|
||||
|
||||
sentBubbles.forEach {
|
||||
it.bubble.background.colorFilter = chatColors.chatBubbleColorFilter
|
||||
}
|
||||
|
||||
val mask: Drawable = chatColors.chatBubbleMask
|
||||
val bubbles = if (bubbleCount == 4) {
|
||||
listOf(sent1, sent2)
|
||||
} else {
|
||||
listOf(sent1)
|
||||
}
|
||||
|
||||
val projections = bubbles.map {
|
||||
Projection.relativeToViewWithCommonRoot(it.bubble, colorizerView, Projection.Corners(ViewUtil.dpToPx(10).toFloat()))
|
||||
}
|
||||
|
||||
colorizerView.setProjections(projections)
|
||||
colorizerView.visibility = View.VISIBLE
|
||||
colorizerView.background = mask
|
||||
|
||||
sentBubbles.forEach {
|
||||
it.body.setTextColor(colorizer.getOutgoingBodyTextColor(context))
|
||||
it.time.setTextColor(colorizer.getOutgoingFooterTextColor(context))
|
||||
it.delivery?.setTint(colorizer.getOutgoingFooterIconColor(context))
|
||||
}
|
||||
}
|
||||
|
||||
private class Bubble(val bubble: View, val body: TextView, val time: TextView, val delivery: DeliveryStatusView?)
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Path
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.util.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.customizeOnDraw
|
||||
|
||||
class ChatColorSelectionAdapter(
|
||||
context: Context,
|
||||
private val callbacks: Callbacks
|
||||
) : MappingAdapter() {
|
||||
init {
|
||||
val popupWindow = ChatSelectionContextMenu(context)
|
||||
|
||||
registerFactory(
|
||||
ChatColorMappingModel::class.java,
|
||||
LayoutFactory(
|
||||
{ v -> ChatColorMappingViewHolder(v, popupWindow, callbacks) },
|
||||
R.layout.chat_color_selection_adapter_item
|
||||
)
|
||||
)
|
||||
registerFactory(
|
||||
CustomColorMappingModel::class.java,
|
||||
LayoutFactory(
|
||||
{ v -> CustomColorMappingViewHolder(v, callbacks::onAdd) },
|
||||
R.layout.chat_color_custom_adapter_item
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
class CustomColorMappingViewHolder(
|
||||
itemView: View,
|
||||
onClicked: () -> Unit
|
||||
) : MappingViewHolder<CustomColorMappingModel>(itemView) {
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener { onClicked() }
|
||||
}
|
||||
|
||||
override fun bind(model: CustomColorMappingModel) = Unit
|
||||
}
|
||||
|
||||
class ChatColorMappingViewHolder(
|
||||
itemView: View,
|
||||
private val popupWindow: ChatSelectionContextMenu,
|
||||
private val callbacks: Callbacks
|
||||
) : MappingViewHolder<ChatColorMappingModel>(itemView) {
|
||||
|
||||
private val preview: ImageView = itemView.findViewById(R.id.chat_color)
|
||||
private val auto: TextView = itemView.findViewById(R.id.auto)
|
||||
private val edit: View = itemView.findViewById(R.id.edit)
|
||||
|
||||
override fun bind(model: ChatColorMappingModel) {
|
||||
itemView.isSelected = model.isSelected
|
||||
|
||||
auto.visibility = if (model.isAuto) View.VISIBLE else View.GONE
|
||||
edit.visibility = if (model.isCustom) View.VISIBLE else View.GONE
|
||||
|
||||
preview.setOnClickListener {
|
||||
if (model.isCustom && model.isSelected) {
|
||||
callbacks.onEdit(model.chatColors)
|
||||
} else {
|
||||
callbacks.onSelect(model.chatColors)
|
||||
}
|
||||
}
|
||||
|
||||
if (model.isCustom) {
|
||||
preview.setOnLongClickListener {
|
||||
popupWindow.callback = CallbackBinder(callbacks, model.chatColors)
|
||||
popupWindow.show(itemView)
|
||||
true
|
||||
}
|
||||
} else {
|
||||
preview.setOnLongClickListener(null)
|
||||
preview.isLongClickable = false
|
||||
}
|
||||
|
||||
val mask = model.chatColors.chatBubbleMask
|
||||
preview.setImageDrawable(
|
||||
mask.customizeOnDraw { wrapped, canvas ->
|
||||
val circlePath = Path()
|
||||
val bounds = canvas.clipBounds
|
||||
circlePath.addCircle(
|
||||
bounds.width() / 2f,
|
||||
bounds.height() / 2f,
|
||||
bounds.width() / 2f,
|
||||
Path.Direction.CW
|
||||
)
|
||||
|
||||
canvas.save()
|
||||
canvas.clipPath(circlePath)
|
||||
wrapped.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class CallbackBinder(private val callbacks: Callbacks, private val chatColors: ChatColors) : ChatSelectionContextMenu.Callback {
|
||||
override fun onEditPressed() {
|
||||
callbacks.onEdit(chatColors)
|
||||
}
|
||||
|
||||
override fun onDuplicatePressed() {
|
||||
callbacks.onDuplicate(chatColors)
|
||||
}
|
||||
|
||||
override fun onDeletePressed() {
|
||||
callbacks.onDelete(chatColors)
|
||||
}
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
fun onSelect(chatColors: ChatColors)
|
||||
fun onEdit(chatColors: ChatColors)
|
||||
fun onDuplicate(chatColors: ChatColors)
|
||||
fun onDelete(chatColors: ChatColors)
|
||||
fun onAdd()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
|
||||
class ChatColorSelectionFragment : Fragment(R.layout.chat_color_selection_fragment) {
|
||||
|
||||
private lateinit var viewModel: ChatColorSelectionViewModel
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val args: ChatColorSelectionFragmentArgs = ChatColorSelectionFragmentArgs.fromBundle(requireArguments())
|
||||
|
||||
viewModel = ChatColorSelectionViewModel.getOrCreate(requireActivity(), args.recipientId)
|
||||
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
val preview: ChatColorPreviewView = view.findViewById(R.id.preview)
|
||||
val recycler: RecyclerView = view.findViewById(R.id.recycler)
|
||||
val adapter = ChatColorSelectionAdapter(
|
||||
requireContext(),
|
||||
Callbacks(args, view)
|
||||
)
|
||||
|
||||
recycler.itemAnimator = null
|
||||
recycler.adapter = adapter
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
Navigation.findNavController(it).popBackStack()
|
||||
}
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
preview.setWallpaper(state.wallpaper)
|
||||
|
||||
if (state.chatColors != null) {
|
||||
preview.setChatColors(state.chatColors)
|
||||
}
|
||||
|
||||
adapter.submitList(state.chatColorModels)
|
||||
}
|
||||
|
||||
viewModel.events.observe(viewLifecycleOwner) { event ->
|
||||
if (event is ChatColorSelectionViewModel.Event.ConfirmDeletion) {
|
||||
showWarningDialog(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel.refresh()
|
||||
}
|
||||
|
||||
private fun showWarningDialog(confirmDeletion: ChatColorSelectionViewModel.Event.ConfirmDeletion) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.ChatColorSelectionFragment__delete_color)
|
||||
.setMessage(resources.getQuantityString(R.plurals.ChatColorSelectionFragment__this_custom_color_is_used, confirmDeletion.usageCount, confirmDeletion.usageCount))
|
||||
.setPositiveButton(R.string.delete) { dialog, _ ->
|
||||
viewModel.deleteNow(confirmDeletion.chatColors)
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
inner class Callbacks(
|
||||
private val args: ChatColorSelectionFragmentArgs,
|
||||
private val view: View
|
||||
) : ChatColorSelectionAdapter.Callbacks {
|
||||
override fun onSelect(chatColors: ChatColors) {
|
||||
viewModel.save(chatColors)
|
||||
}
|
||||
|
||||
override fun onEdit(chatColors: ChatColors) {
|
||||
val startPage = if (chatColors.getColors().size == 1) 0 else 1
|
||||
|
||||
val directions = ChatColorSelectionFragmentDirections
|
||||
.actionChatColorSelectionFragmentToCustomChatColorCreatorFragment(args.recipientId, startPage)
|
||||
.setChatColorId(chatColors.id.longValue)
|
||||
|
||||
Navigation.findNavController(view).navigate(directions)
|
||||
}
|
||||
|
||||
override fun onDuplicate(chatColors: ChatColors) {
|
||||
viewModel.duplicate(chatColors)
|
||||
}
|
||||
|
||||
override fun onDelete(chatColors: ChatColors) {
|
||||
viewModel.startDeletion(chatColors)
|
||||
}
|
||||
|
||||
override fun onAdd() {
|
||||
val directions = ChatColorSelectionFragmentDirections.actionChatColorSelectionFragmentToCustomChatColorCreatorFragment(args.recipientId, 0)
|
||||
Navigation.findNavController(view).navigate(directions)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
|
||||
sealed class ChatColorSelectionRepository(context: Context) {
|
||||
|
||||
protected val context: Context = context.applicationContext
|
||||
|
||||
abstract fun getWallpaper(consumer: (ChatWallpaper?) -> Unit)
|
||||
abstract fun getChatColors(consumer: (ChatColors) -> Unit)
|
||||
abstract fun save(chatColors: ChatColors, onSaved: () -> Unit)
|
||||
|
||||
fun duplicate(chatColors: ChatColors) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val duplicate = chatColors.withId(ChatColors.Id.NotSet)
|
||||
DatabaseFactory.getChatColorsDatabase(context).saveChatColors(duplicate)
|
||||
}
|
||||
}
|
||||
|
||||
fun getUsageCount(chatColors: ChatColors, consumer: (Int) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(DatabaseFactory.getRecipientDatabase(context).getColorUsageCount(chatColors))
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(chatColors: ChatColors, onDeleted: () -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
DatabaseFactory.getChatColorsDatabase(context).deleteChatColors(chatColors)
|
||||
onDeleted()
|
||||
}
|
||||
}
|
||||
|
||||
private class Global(context: Context) : ChatColorSelectionRepository(context) {
|
||||
override fun getWallpaper(consumer: (ChatWallpaper?) -> Unit) {
|
||||
consumer(SignalStore.wallpaper().wallpaper)
|
||||
}
|
||||
|
||||
override fun getChatColors(consumer: (ChatColors) -> Unit) {
|
||||
if (SignalStore.chatColorsValues().hasChatColors) {
|
||||
consumer(requireNotNull(SignalStore.chatColorsValues().chatColors))
|
||||
} else {
|
||||
getWallpaper { wallpaper ->
|
||||
if (wallpaper != null) {
|
||||
consumer(wallpaper.autoChatColors)
|
||||
} else {
|
||||
consumer(ChatColorsPalette.Bubbles.default)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun save(chatColors: ChatColors, onSaved: () -> Unit) {
|
||||
if (chatColors.id == ChatColors.Id.Auto) {
|
||||
SignalStore.chatColorsValues().chatColors = null
|
||||
} else {
|
||||
SignalStore.chatColorsValues().chatColors = chatColors
|
||||
}
|
||||
onSaved()
|
||||
}
|
||||
}
|
||||
|
||||
private class Single(context: Context, private val recipientId: RecipientId) : ChatColorSelectionRepository(context) {
|
||||
override fun getWallpaper(consumer: (ChatWallpaper?) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
consumer(recipient.wallpaper)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChatColors(consumer: (ChatColors) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
consumer(recipient.chatColors)
|
||||
}
|
||||
}
|
||||
|
||||
override fun save(chatColors: ChatColors, onSaved: () -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
recipientDatabase.setColor(recipientId, chatColors)
|
||||
onSaved()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(context: Context, recipientId: RecipientId?): ChatColorSelectionRepository {
|
||||
return if (recipientId != null) {
|
||||
Single(context, recipientId)
|
||||
} else {
|
||||
Global(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
|
||||
import org.thoughtcrime.securesms.util.MappingModelList
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
|
||||
data class ChatColorSelectionState(
|
||||
val wallpaper: ChatWallpaper? = null,
|
||||
val chatColors: ChatColors? = null,
|
||||
private val chatColorOptions: List<ChatColors> = listOf()
|
||||
) {
|
||||
|
||||
val chatColorModels: MappingModelList
|
||||
|
||||
init {
|
||||
val models: List<ChatColorMappingModel> = chatColorOptions.map { chatColors ->
|
||||
ChatColorMappingModel(
|
||||
chatColors,
|
||||
chatColors == this.chatColors,
|
||||
false
|
||||
)
|
||||
}.toList()
|
||||
|
||||
val defaultModel: ChatColorMappingModel = if (wallpaper != null) {
|
||||
ChatColorMappingModel(
|
||||
wallpaper.autoChatColors,
|
||||
chatColors?.id == ChatColors.Id.Auto,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
ChatColorMappingModel(
|
||||
ChatColorsPalette.Bubbles.default,
|
||||
chatColors?.id == ChatColors.Id.Auto,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
chatColorModels = MappingModelList().apply {
|
||||
add(defaultModel)
|
||||
addAll(models)
|
||||
add(CustomColorMappingModel())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class ChatColorSelectionViewModel(private val repository: ChatColorSelectionRepository) : ViewModel() {
|
||||
|
||||
private val store = Store<ChatColorSelectionState>(ChatColorSelectionState())
|
||||
private val chatColors = ChatColorsOptionsLiveData()
|
||||
private val internalEvents = SingleLiveEvent<Event>()
|
||||
|
||||
val state: LiveData<ChatColorSelectionState> = store.stateLiveData
|
||||
val events: LiveData<Event> = internalEvents
|
||||
|
||||
init {
|
||||
store.update(chatColors) { colors, state -> state.copy(chatColorOptions = colors) }
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
repository.getWallpaper { wallpaper ->
|
||||
store.update { it.copy(wallpaper = wallpaper) }
|
||||
}
|
||||
|
||||
repository.getChatColors { chatColors ->
|
||||
store.update { it.copy(chatColors = chatColors) }
|
||||
}
|
||||
}
|
||||
|
||||
fun save(chatColors: ChatColors) {
|
||||
repository.save(chatColors, this::refresh)
|
||||
}
|
||||
|
||||
fun duplicate(chatColors: ChatColors) {
|
||||
repository.duplicate(chatColors)
|
||||
}
|
||||
|
||||
fun startDeletion(chatColors: ChatColors) {
|
||||
repository.getUsageCount(chatColors) {
|
||||
if (it > 0) {
|
||||
internalEvents.postValue(Event.ConfirmDeletion(it, chatColors))
|
||||
} else {
|
||||
deleteNow(chatColors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteNow(chatColors: ChatColors) {
|
||||
repository.delete(chatColors, this::refresh)
|
||||
}
|
||||
|
||||
class Factory(private val repository: ChatColorSelectionRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T = requireNotNull(modelClass.cast(ChatColorSelectionViewModel(repository)))
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getOrCreate(activity: FragmentActivity, recipientId: RecipientId?): ChatColorSelectionViewModel {
|
||||
val repository = ChatColorSelectionRepository.create(activity, recipientId)
|
||||
val viewModelFactory = Factory(repository)
|
||||
|
||||
return ViewModelProviders.of(activity, viewModelFactory).get(ChatColorSelectionViewModel::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Event {
|
||||
class ConfirmDeletion(val usageCount: Int, val chatColors: ChatColors) : Event()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
|
||||
import org.thoughtcrime.securesms.database.ChatColorsDatabase
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class ChatColorsOptionsLiveData : LiveData<List<ChatColors>>() {
|
||||
private val chatColorsDatabase: ChatColorsDatabase = DatabaseFactory.getChatColorsDatabase(ApplicationDependencies.getApplication())
|
||||
private val observer: DatabaseObserver.Observer = DatabaseObserver.Observer { refreshChatColors() }
|
||||
private val executor: Executor = SerialMonoLifoExecutor(SignalExecutors.BOUNDED)
|
||||
|
||||
override fun onActive() {
|
||||
refreshChatColors()
|
||||
ApplicationDependencies.getDatabaseObserver().registerChatColorsObserver(observer)
|
||||
}
|
||||
|
||||
override fun onInactive() {
|
||||
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer)
|
||||
}
|
||||
|
||||
private fun refreshChatColors() {
|
||||
executor.execute {
|
||||
val options = mutableListOf<ChatColors>().apply {
|
||||
addAll(ChatColorsPalette.Bubbles.all)
|
||||
addAll(chatColorsDatabase.getSavedChatColors())
|
||||
}
|
||||
|
||||
postValue(options)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupWindow
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
class ChatSelectionContextMenu(val context: Context) : PopupWindow(context) {
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
init {
|
||||
contentView = LayoutInflater.from(context).inflate(R.layout.chat_colors_fragment_context_menu, null, false)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
elevation = ViewUtil.dpToPx(8).toFloat()
|
||||
}
|
||||
|
||||
isOutsideTouchable = false
|
||||
isFocusable = true
|
||||
|
||||
width = ViewUtil.dpToPx(280)
|
||||
|
||||
setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.round_background))
|
||||
|
||||
val edit: View = contentView.findViewById(R.id.context_menu_edit)
|
||||
val duplicate: View = contentView.findViewById(R.id.context_menu_duplicate)
|
||||
val delete: View = contentView.findViewById(R.id.context_menu_delete)
|
||||
|
||||
edit.setOnClickListener {
|
||||
dismiss()
|
||||
callback?.onEditPressed()
|
||||
}
|
||||
|
||||
duplicate.setOnClickListener {
|
||||
dismiss()
|
||||
callback?.onDuplicatePressed()
|
||||
}
|
||||
|
||||
delete.setOnClickListener {
|
||||
dismiss()
|
||||
callback?.onDeletePressed()
|
||||
}
|
||||
}
|
||||
|
||||
fun show(anchor: View) {
|
||||
val rect = Rect()
|
||||
val root: ViewGroup = anchor.rootView as ViewGroup
|
||||
|
||||
anchor.getDrawingRect(rect)
|
||||
root.offsetDescendantRectToMyCoords(anchor, rect)
|
||||
|
||||
if (rect.bottom + contentView.height > root.bottom) {
|
||||
showAsDropDown(anchor, 0, -(contentView.height + anchor.height))
|
||||
} else {
|
||||
showAsDropDown(anchor, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onEditPressed()
|
||||
fun onDuplicatePressed()
|
||||
fun onDeletePressed()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui
|
||||
|
||||
import org.thoughtcrime.securesms.util.MappingModel
|
||||
|
||||
class CustomColorMappingModel : MappingModel<CustomColorMappingModel> {
|
||||
override fun areItemsTheSame(newItem: CustomColorMappingModel): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: CustomColorMappingModel): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui.custom
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
class CustomChatColorCreatorFragment : Fragment(R.layout.custom_chat_color_creator_fragment) {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val toolbar: Toolbar = view.findViewById(R.id.toolbar)
|
||||
val tabLayout: TabLayout = view.findViewById(R.id.tab_layout)
|
||||
val pager: ViewPager2 = view.findViewById(R.id.pager)
|
||||
val adapter = CustomChatColorPagerAdapter(this, requireArguments())
|
||||
val tabLayoutMediator = TabLayoutMediator(tabLayout, pager) { tab, position ->
|
||||
tab.setText(
|
||||
if (position == 0) {
|
||||
R.string.CustomChatColorCreatorFragment__solid
|
||||
} else {
|
||||
R.string.CustomChatColorCreatorFragment__gradient
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
toolbar.setNavigationOnClickListener {
|
||||
Navigation.findNavController(it).popBackStack()
|
||||
}
|
||||
|
||||
pager.isUserInputEnabled = false
|
||||
pager.adapter = adapter
|
||||
tabLayoutMediator.attach()
|
||||
|
||||
val startPage = CustomChatColorCreatorFragmentArgs.fromBundle(requireArguments()).startPage
|
||||
pager.setCurrentItem(startPage, false)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,366 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui.custom
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.PointF
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Dimension
|
||||
import androidx.appcompat.widget.AppCompatSeekBar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.navigation.Navigation
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.conversation.colors.ui.ChatColorPreviewView
|
||||
import org.thoughtcrime.securesms.conversation.colors.ui.ChatColorSelectionViewModel
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.customizeOnDraw
|
||||
|
||||
private const val MAX_SEEK_DIVISIONS = 1023
|
||||
private const val MAX_HUE = 360
|
||||
|
||||
private const val PAGE_ARG = "page"
|
||||
private const val SINGLE_PAGE = 0
|
||||
private const val GRADIENT_PAGE = 1
|
||||
|
||||
class CustomChatColorCreatorPageFragment :
|
||||
Fragment(R.layout.custom_chat_color_creator_fragment_page) {
|
||||
|
||||
private lateinit var hueSlider: AppCompatSeekBar
|
||||
private lateinit var saturationSlider: AppCompatSeekBar
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val args: CustomChatColorCreatorFragmentArgs = CustomChatColorCreatorFragmentArgs.fromBundle(requireArguments())
|
||||
val chatColorSelectionViewModel: ChatColorSelectionViewModel = ChatColorSelectionViewModel.getOrCreate(requireActivity(), args.recipientId)
|
||||
val page: Int = requireArguments().getInt(PAGE_ARG)
|
||||
val factory: CustomChatColorCreatorViewModel.Factory = CustomChatColorCreatorViewModel.Factory(MAX_SEEK_DIVISIONS, ChatColors.Id.forLongValue(args.chatColorId), args.recipientId, createRepository())
|
||||
val viewModel: CustomChatColorCreatorViewModel = ViewModelProviders.of(
|
||||
requireParentFragment(),
|
||||
factory
|
||||
)[CustomChatColorCreatorViewModel::class.java]
|
||||
|
||||
val preview: ChatColorPreviewView = view.findViewById(R.id.chat_color_preview)
|
||||
val hueThumb = ThumbDrawable(requireContext())
|
||||
val saturationThumb = ThumbDrawable(requireContext())
|
||||
|
||||
val gradientTool: CustomChatColorGradientToolView = view.findViewById(R.id.gradient_tool)
|
||||
val save: View = view.findViewById(R.id.save)
|
||||
|
||||
if (page == SINGLE_PAGE) {
|
||||
gradientTool.visibility = View.GONE
|
||||
} else {
|
||||
gradientTool.setListener(object : CustomChatColorGradientToolView.Listener {
|
||||
override fun onDegreesChanged(degrees: Float) {
|
||||
viewModel.setDegrees(degrees)
|
||||
}
|
||||
|
||||
override fun onSelectedEdgeChanged(edge: CustomChatColorEdge) {
|
||||
viewModel.setSelectedEdge(edge)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
hueSlider = view.findViewById(R.id.hue_slider)
|
||||
saturationSlider = view.findViewById(R.id.saturation_slider)
|
||||
|
||||
hueSlider.thumb = hueThumb
|
||||
saturationSlider.thumb = saturationThumb
|
||||
|
||||
hueSlider.max = MAX_SEEK_DIVISIONS
|
||||
saturationSlider.max = MAX_SEEK_DIVISIONS
|
||||
|
||||
val colors: IntArray = (0..MAX_SEEK_DIVISIONS).map { hue ->
|
||||
ColorUtils.HSLToColor(
|
||||
floatArrayOf(
|
||||
hue.toHue(MAX_SEEK_DIVISIONS),
|
||||
1f,
|
||||
calculateLightness(hue.toFloat(), valueFor60To80 = 0.4f)
|
||||
)
|
||||
)
|
||||
}.toIntArray()
|
||||
|
||||
val hueGradientDrawable = GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, colors)
|
||||
|
||||
hueSlider.progressDrawable = hueGradientDrawable.forSeekBar()
|
||||
|
||||
val saturationProgressDrawable = GradientDrawable().apply {
|
||||
orientation = GradientDrawable.Orientation.LEFT_RIGHT
|
||||
}
|
||||
|
||||
saturationSlider.progressDrawable = saturationProgressDrawable.forSeekBar()
|
||||
|
||||
hueSlider.setOnSeekBarChangeListener(
|
||||
OnProgressChangedListener {
|
||||
viewModel.setHueProgress(it)
|
||||
}
|
||||
)
|
||||
|
||||
saturationSlider.setOnSeekBarChangeListener(
|
||||
OnProgressChangedListener {
|
||||
viewModel.setSaturationProgress(it)
|
||||
}
|
||||
)
|
||||
|
||||
viewModel.events.observe(viewLifecycleOwner) { event ->
|
||||
when (event) {
|
||||
is CustomChatColorCreatorViewModel.Event.SaveNow -> {
|
||||
viewModel.saveNow(event.chatColors) { colors ->
|
||||
chatColorSelectionViewModel.save(colors)
|
||||
}
|
||||
Navigation.findNavController(requireParentFragment().requireView()).popBackStack()
|
||||
}
|
||||
is CustomChatColorCreatorViewModel.Event.Warn -> MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.CustomChatColorCreatorFragmentPage__edit_color)
|
||||
.setMessage(resources.getQuantityString(R.plurals.CustomChatColorCreatorFragmentPage__this_color_is_used, event.usageCount, event.usageCount))
|
||||
.setPositiveButton(R.string.save) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
viewModel.saveNow(event.chatColors) { colors ->
|
||||
chatColorSelectionViewModel.save(colors)
|
||||
}
|
||||
Navigation.findNavController(requireParentFragment().requireView()).popBackStack()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
if (state.loading) {
|
||||
return@observe
|
||||
}
|
||||
|
||||
val sliderState: ColorSlidersState = requireNotNull(state.sliderStates[state.selectedEdge])
|
||||
|
||||
hueSlider.progress = sliderState.huePosition
|
||||
saturationSlider.progress = sliderState.saturationPosition
|
||||
|
||||
val color: Int = sliderState.getColor()
|
||||
hueThumb.setColor(sliderState.getHueColor())
|
||||
saturationThumb.setColor(color)
|
||||
saturationProgressDrawable.colors = sliderState.getSaturationColors()
|
||||
|
||||
preview.setWallpaper(state.wallpaper)
|
||||
|
||||
if (page == 0) {
|
||||
val chatColor = ChatColors.forColor(ChatColors.Id.NotSet, color)
|
||||
preview.setChatColors(chatColor)
|
||||
save.setOnClickListener {
|
||||
viewModel.startSave(chatColor)
|
||||
}
|
||||
} else {
|
||||
val topEdgeColor: ColorSlidersState = requireNotNull(state.sliderStates[CustomChatColorEdge.TOP])
|
||||
val bottomEdgeColor: ColorSlidersState = requireNotNull(state.sliderStates[CustomChatColorEdge.BOTTOM])
|
||||
val chatColor: ChatColors = ChatColors.forGradient(
|
||||
ChatColors.Id.NotSet,
|
||||
ChatColors.LinearGradient(
|
||||
state.degrees,
|
||||
intArrayOf(topEdgeColor.getColor(), bottomEdgeColor.getColor()),
|
||||
floatArrayOf(0f, 1f)
|
||||
),
|
||||
)
|
||||
preview.setChatColors(chatColor)
|
||||
gradientTool.setSelectedEdge(state.selectedEdge)
|
||||
gradientTool.setDegrees(state.degrees)
|
||||
gradientTool.setTopColor(topEdgeColor.getColor())
|
||||
gradientTool.setBottomColor(bottomEdgeColor.getColor())
|
||||
save.setOnClickListener {
|
||||
viewModel.startSave(chatColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRepository(): CustomChatColorCreatorRepository {
|
||||
return CustomChatColorCreatorRepository(requireContext())
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
private fun ColorSlidersState.getHueColor(): Int {
|
||||
val hue = huePosition.toHue(MAX_SEEK_DIVISIONS)
|
||||
return ColorUtils.HSLToColor(
|
||||
floatArrayOf(
|
||||
hue,
|
||||
1f,
|
||||
calculateLightness(hue, 0.4f)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
private fun ColorSlidersState.getColor(): Int {
|
||||
val hue = huePosition.toHue(MAX_SEEK_DIVISIONS)
|
||||
return ColorUtils.HSLToColor(
|
||||
floatArrayOf(
|
||||
hue,
|
||||
saturationPosition.toUnit(MAX_SEEK_DIVISIONS),
|
||||
calculateLightness(hue)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun ColorSlidersState.getSaturationColors(): IntArray {
|
||||
val hue = huePosition.toHue(MAX_SEEK_DIVISIONS)
|
||||
val level = calculateLightness(hue)
|
||||
|
||||
return listOf(0f, 1f).map {
|
||||
ColorUtils.HSLToColor(
|
||||
floatArrayOf(
|
||||
hue, it, level
|
||||
)
|
||||
)
|
||||
}.toIntArray()
|
||||
}
|
||||
|
||||
private fun calculateLightness(hue: Float, valueFor60To80: Float = 0.3f): Float {
|
||||
|
||||
val point1 = PointF()
|
||||
val point2 = PointF()
|
||||
|
||||
if (hue >= 0f && hue < 60f) {
|
||||
point1.set(0f, 0.45f)
|
||||
point2.set(60f, valueFor60To80)
|
||||
} else if (hue >= 60f && hue < 180f) {
|
||||
return valueFor60To80
|
||||
} else if (hue >= 180f && hue < 240f) {
|
||||
point1.set(180f, valueFor60To80)
|
||||
point2.set(240f, 0.5f)
|
||||
} else if (hue >= 240f && hue < 300f) {
|
||||
point1.set(240f, 0.5f)
|
||||
point2.set(300f, 0.4f)
|
||||
} else if (hue >= 300f && hue < 360f) {
|
||||
point1.set(300f, 0.4f)
|
||||
point2.set(360f, 0.45f)
|
||||
} else {
|
||||
return 0.45f
|
||||
}
|
||||
|
||||
return interpolate(point1, point2, hue)
|
||||
}
|
||||
|
||||
private fun interpolate(point1: PointF, point2: PointF, x: Float): Float {
|
||||
return ((point1.y * (point2.x - x)) + (point2.y * (x - point1.x))) / (point2.x - point1.x)
|
||||
}
|
||||
|
||||
private fun Number.toHue(max: Number): Float {
|
||||
return Util.clamp(toFloat() * (MAX_HUE / max.toFloat()), 0f, MAX_HUE.toFloat())
|
||||
}
|
||||
|
||||
private fun Number.toUnit(max: Number): Float {
|
||||
return Util.clamp(toFloat() / max.toFloat(), 0f, 1f)
|
||||
}
|
||||
|
||||
private fun Drawable.forSeekBar(): Drawable {
|
||||
val height: Int = ViewUtil.dpToPx(8)
|
||||
val radii: FloatArray = (1..8).map { 50f }.toFloatArray()
|
||||
val bounds = RectF()
|
||||
val clipPath = Path()
|
||||
|
||||
return customizeOnDraw { wrapped, canvas ->
|
||||
canvas.save()
|
||||
bounds.set(this.bounds)
|
||||
bounds.inset(0f, (height / 2f) + 1)
|
||||
|
||||
clipPath.rewind()
|
||||
clipPath.addRoundRect(bounds, radii, Path.Direction.CW)
|
||||
|
||||
canvas.clipPath(clipPath)
|
||||
wrapped.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
|
||||
private class OnProgressChangedListener(private val updateFn: (Int) -> Unit) :
|
||||
SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
updateFn(progress)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) = Unit
|
||||
}
|
||||
|
||||
private class ThumbDrawable(context: Context) : Drawable() {
|
||||
|
||||
private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = ContextCompat.getColor(context, R.color.signal_background_primary)
|
||||
}
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.TRANSPARENT
|
||||
}
|
||||
|
||||
private val borderWidth: Int = ViewUtil.dpToPx(THUMB_MARGIN)
|
||||
private val thumbInnerSize: Int = ViewUtil.dpToPx(THUMB_INNER_SIZE)
|
||||
private val innerRadius: Float = thumbInnerSize / 2f
|
||||
private val thumbSize: Float = (thumbInnerSize + borderWidth).toFloat()
|
||||
private val thumbRadius: Float = thumbSize / 2f
|
||||
|
||||
override fun getIntrinsicHeight(): Int = ViewUtil.dpToPx(48)
|
||||
|
||||
override fun getIntrinsicWidth(): Int = ViewUtil.dpToPx(48)
|
||||
|
||||
fun setColor(@ColorInt color: Int) {
|
||||
paint.color = color
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.drawCircle(
|
||||
(bounds.width() / 2f) + bounds.left,
|
||||
(bounds.height() / 2f) + bounds.top,
|
||||
thumbRadius,
|
||||
borderPaint
|
||||
)
|
||||
canvas.drawCircle(
|
||||
(bounds.width() / 2f) + bounds.left,
|
||||
(bounds.height() / 2f) + bounds.top,
|
||||
innerRadius,
|
||||
paint
|
||||
)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) = Unit
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
override fun getOpacity(): Int = PixelFormat.TRANSPARENT
|
||||
|
||||
companion object {
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private val THUMB_INNER_SIZE = 16
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private val THUMB_MARGIN = 1
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun forSingle(bundle: Bundle): Fragment = forPage(SINGLE_PAGE, bundle)
|
||||
|
||||
fun forGradient(bundle: Bundle): Fragment = forPage(GRADIENT_PAGE, bundle)
|
||||
|
||||
private fun forPage(page: Int, bundle: Bundle): Fragment = CustomChatColorCreatorPageFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putInt(PAGE_ARG, page)
|
||||
putAll(bundle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui.custom
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
|
||||
class CustomChatColorCreatorRepository(private val context: Context) {
|
||||
fun loadColors(chatColorsId: ChatColors.Id, consumer: (ChatColors) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val chatColorsDatabase = DatabaseFactory.getChatColorsDatabase(context)
|
||||
val chatColors = chatColorsDatabase.getById(chatColorsId)
|
||||
|
||||
consumer(chatColors)
|
||||
}
|
||||
}
|
||||
|
||||
fun getWallpaper(recipientId: RecipientId?, consumer: (ChatWallpaper?) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
if (recipientId != null) {
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
consumer(recipient.wallpaper)
|
||||
} else {
|
||||
consumer(SignalStore.wallpaper().wallpaper)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setChatColors(chatColors: ChatColors, consumer: (ChatColors) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val chatColorsDatabase = DatabaseFactory.getChatColorsDatabase(context)
|
||||
val savedColors = chatColorsDatabase.saveChatColors(chatColors)
|
||||
|
||||
consumer(savedColors)
|
||||
}
|
||||
}
|
||||
|
||||
fun getUsageCount(chatColors: ChatColors, consumer: (Int) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
val recipientsDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
|
||||
consumer(recipientsDatabase.getColorUsageCount(chatColors))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui.custom
|
||||
|
||||
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
|
||||
import java.util.EnumMap
|
||||
|
||||
data class CustomChatColorCreatorState(
|
||||
val loading: Boolean,
|
||||
val wallpaper: ChatWallpaper?,
|
||||
val sliderStates: EnumMap<CustomChatColorEdge, ColorSlidersState>,
|
||||
val selectedEdge: CustomChatColorEdge,
|
||||
val degrees: Float
|
||||
)
|
||||
|
||||
data class ColorSlidersState(val huePosition: Int, val saturationPosition: Int)
|
|
@ -0,0 +1,153 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui.custom
|
||||
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
import java.util.EnumMap
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class CustomChatColorCreatorViewModel(
|
||||
private val maxSliderValue: Int,
|
||||
private val chatColorsId: ChatColors.Id,
|
||||
private val recipientId: RecipientId?,
|
||||
private val repository: CustomChatColorCreatorRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val store = Store<CustomChatColorCreatorState>(getInitialState())
|
||||
private val internalEvents = SingleLiveEvent<Event>()
|
||||
|
||||
val state: LiveData<CustomChatColorCreatorState> = store.stateLiveData
|
||||
val events: LiveData<Event> = internalEvents
|
||||
|
||||
init {
|
||||
repository.getWallpaper(recipientId) { wallpaper ->
|
||||
store.update { it.copy(wallpaper = wallpaper) }
|
||||
}
|
||||
|
||||
if (chatColorsId is ChatColors.Id.Custom) {
|
||||
repository.loadColors(chatColorsId) {
|
||||
val colors: IntArray = it.getColors()
|
||||
|
||||
val topColor: Int = colors.first()
|
||||
val bottomColor: Int = colors.last()
|
||||
|
||||
val topHsl = floatArrayOf(0f, 0f, 0f)
|
||||
val bottomHsl = floatArrayOf(0f, 0f, 0f)
|
||||
|
||||
ColorUtils.colorToHSL(topColor, topHsl)
|
||||
ColorUtils.colorToHSL(bottomColor, bottomHsl)
|
||||
|
||||
val topHue: Float = topHsl[0]
|
||||
val topSaturation: Float = topHsl[1]
|
||||
|
||||
val bottomHue: Float = bottomHsl[0]
|
||||
val bottomSaturation: Float = bottomHsl[1]
|
||||
|
||||
val topEdge = ColorSlidersState(
|
||||
huePosition = ((topHue / 360f) * maxSliderValue).roundToInt(),
|
||||
saturationPosition = (topSaturation * maxSliderValue).roundToInt()
|
||||
)
|
||||
|
||||
val bottomEdge = ColorSlidersState(
|
||||
huePosition = ((bottomHue / 360f) * maxSliderValue).roundToInt(),
|
||||
saturationPosition = (bottomSaturation * maxSliderValue).roundToInt()
|
||||
)
|
||||
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
degrees = it.getDegrees(),
|
||||
loading = false,
|
||||
sliderStates = EnumMap(
|
||||
mapOf(
|
||||
CustomChatColorEdge.TOP to topEdge,
|
||||
CustomChatColorEdge.BOTTOM to bottomEdge
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setHueProgress(progress: Int) {
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
sliderStates = state.sliderStates.apply {
|
||||
val oldData: ColorSlidersState = requireNotNull(get(state.selectedEdge))
|
||||
put(state.selectedEdge, oldData.copy(huePosition = progress))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSaturationProgress(progress: Int) {
|
||||
store.update { state ->
|
||||
state.copy(
|
||||
sliderStates = state.sliderStates.apply {
|
||||
val oldData: ColorSlidersState = requireNotNull(get(state.selectedEdge))
|
||||
put(state.selectedEdge, oldData.copy(saturationPosition = progress))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setDegrees(degrees: Float) {
|
||||
store.update { it.copy(degrees = degrees) }
|
||||
}
|
||||
|
||||
fun setSelectedEdge(selectedEdge: CustomChatColorEdge) {
|
||||
store.update { it.copy(selectedEdge = selectedEdge) }
|
||||
}
|
||||
|
||||
fun startSave(chatColors: ChatColors) {
|
||||
if (chatColors.id is ChatColors.Id.Custom) {
|
||||
repository.getUsageCount(chatColors) {
|
||||
if (it > 0) {
|
||||
internalEvents.postValue(Event.Warn(it, chatColors))
|
||||
} else {
|
||||
internalEvents.postValue(Event.SaveNow(chatColors))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
internalEvents.postValue(Event.SaveNow(chatColors))
|
||||
}
|
||||
}
|
||||
|
||||
fun saveNow(chatColors: ChatColors, onSaved: (ChatColors) -> Unit) {
|
||||
repository.setChatColors(chatColors.withId(chatColorsId), onSaved)
|
||||
}
|
||||
|
||||
private fun getInitialState() = CustomChatColorCreatorState(
|
||||
loading = chatColorsId is ChatColors.Id.Custom,
|
||||
wallpaper = null,
|
||||
sliderStates = EnumMap(
|
||||
mapOf(
|
||||
CustomChatColorEdge.TOP to ColorSlidersState(maxSliderValue / 2, maxSliderValue / 2),
|
||||
CustomChatColorEdge.BOTTOM to ColorSlidersState(maxSliderValue / 2, maxSliderValue / 2)
|
||||
)
|
||||
),
|
||||
selectedEdge = CustomChatColorEdge.BOTTOM,
|
||||
degrees = 180f
|
||||
)
|
||||
|
||||
class Factory(
|
||||
private val maxSliderValue: Int,
|
||||
private val chatColorsId: ChatColors.Id,
|
||||
private val recipientId: RecipientId?,
|
||||
private val chatColorCreatorRepository: CustomChatColorCreatorRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(CustomChatColorCreatorViewModel(maxSliderValue, chatColorsId, recipientId, chatColorCreatorRepository)))
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Event {
|
||||
class Warn(val usageCount: Int, val chatColors: ChatColors) : Event()
|
||||
class SaveNow(val chatColors: ChatColors) : Event()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui.custom
|
||||
|
||||
enum class CustomChatColorEdge {
|
||||
TOP, BOTTOM
|
||||
}
|
|
@ -0,0 +1,335 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui.custom
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Dimension
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.atan
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.math.tan
|
||||
|
||||
/**
|
||||
* Renders the gradient customization tool.
|
||||
*
|
||||
* The Gradient customization tool is two selectable circles on either side
|
||||
* of a rectangle with a pipe connecting them, a TOP and a BOTTOM (an edge)
|
||||
*
|
||||
* The user can then swap between the selected edge via a touch-down and can
|
||||
* drag the selected edge such that it traces around the outline of the square.
|
||||
* The other edge traces along the opposite side of the rectangle.
|
||||
*
|
||||
* The way the position along the edge is determined is by dividing the rectangle
|
||||
* into 8 right-angled triangles, all joining at the center. Using the specified
|
||||
* angle, we can determine which "octant" the top edge should be in, and can
|
||||
* determine its distance from the center point of the relevant edge, and use
|
||||
* similar logic to determine where the bottom edge lies.
|
||||
*
|
||||
* All of the math assumes an origin at the dead center of the view, and
|
||||
* that 0deg corresponds to a vector pointing directly towards the right hand
|
||||
* side of the view. This doesn't quite line up with what the gradient rendering
|
||||
* math requires, so we apply a simple function to degrees when it comes into or
|
||||
* leaves this tool (see `Float.invert`)
|
||||
*/
|
||||
class CustomChatColorGradientToolView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val clipRect = Rect()
|
||||
private val rect = RectF()
|
||||
private val center = PointF()
|
||||
private val top = PointF()
|
||||
private val bottom = PointF()
|
||||
|
||||
private var selectedEdge: CustomChatColorEdge = CustomChatColorEdge.TOP
|
||||
private var degrees: Float = 18f
|
||||
private var listener: Listener? = null
|
||||
|
||||
private val thumbRadius: Float = ViewUtil.dpToPx(THUMB_RADIUS).toFloat()
|
||||
private val thumbBorder: Float = ViewUtil.dpToPx(THUMB_BORDER).toFloat()
|
||||
private val thumbBorderSelected: Float = ViewUtil.dpToPx(THUMB_BORDER_SELECTED).toFloat()
|
||||
private val opaqueThumbRadius: Float = ViewUtil.dpToPx(OPAQUE_THUMB_RADIUS).toFloat()
|
||||
private val opaqueThumbPadding: Float = ViewUtil.dpToPx(OPAGUE_THUMB_PADDING).toFloat()
|
||||
private val opaqueThumbPaddingSelected: Float = ViewUtil.dpToPx(OPAGUE_THUMB_PADDING_SELECTED).toFloat()
|
||||
private val pipeWidth: Float = ViewUtil.dpToPx(PIPE_WIDTH).toFloat()
|
||||
private val pipeBorder: Float = ViewUtil.dpToPx(PIPE_BORDER).toFloat()
|
||||
|
||||
private val topColorPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.RED
|
||||
}
|
||||
|
||||
private val bottomColorPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.BLUE
|
||||
}
|
||||
|
||||
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = ContextCompat.getColor(context, R.color.signal_background_primary)
|
||||
}
|
||||
|
||||
private val thumbBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = ContextCompat.getColor(context, R.color.signal_inverse_transparent_10)
|
||||
}
|
||||
|
||||
private val thumbBorderPaintSelected = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = ContextCompat.getColor(context, R.color.signal_inverse_transparent_60)
|
||||
}
|
||||
|
||||
private val pipePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
strokeWidth = pipeWidth - pipeBorder * 2
|
||||
style = Paint.Style.STROKE
|
||||
color = ContextCompat.getColor(context, R.color.signal_background_primary)
|
||||
}
|
||||
|
||||
private val pipeBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
strokeWidth = pipeWidth
|
||||
style = Paint.Style.STROKE
|
||||
color = ContextCompat.getColor(context, R.color.signal_inverse_transparent_10)
|
||||
}
|
||||
|
||||
val gestureDetectorCompat = GestureDetectorCompat(context, GestureListener())
|
||||
|
||||
fun setTopColor(@ColorInt color: Int) {
|
||||
topColorPaint.color = color
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun setBottomColor(@ColorInt color: Int) {
|
||||
bottomColorPaint.color = color
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun setSelectedEdge(selectedEdge: CustomChatColorEdge) {
|
||||
if (this.selectedEdge == selectedEdge) {
|
||||
return
|
||||
}
|
||||
|
||||
this.selectedEdge = selectedEdge
|
||||
invalidate()
|
||||
|
||||
listener?.onSelectedEdgeChanged(selectedEdge)
|
||||
}
|
||||
|
||||
fun setDegrees(degrees: Float) {
|
||||
setDegreesInternal(degrees.invertDegrees())
|
||||
}
|
||||
|
||||
private fun setDegreesInternal(degrees: Float) {
|
||||
if (this.degrees == degrees) {
|
||||
return
|
||||
}
|
||||
|
||||
this.degrees = degrees
|
||||
invalidate()
|
||||
|
||||
listener?.onDegreesChanged(degrees.invertDegrees())
|
||||
}
|
||||
|
||||
private fun Float.invertDegrees(): Float = 360f - rotate(90f)
|
||||
|
||||
fun setListener(listener: Listener) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent?): Boolean {
|
||||
return gestureDetectorCompat.onTouchEvent(event)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
canvas.getClipBounds(clipRect)
|
||||
rect.set(clipRect)
|
||||
rect.inset(thumbRadius, thumbRadius)
|
||||
|
||||
center.set(rect.width() / 2f, rect.height() / 2f)
|
||||
|
||||
val alpha = atan((rect.height() / rect.width())).toDegrees()
|
||||
val beta = (360.0 - alpha * 4) / 4f
|
||||
|
||||
if (degrees < alpha) {
|
||||
// Right top
|
||||
val a = center.x
|
||||
val b = a * tan(degrees.toRadians())
|
||||
top.set(rect.width(), center.y - b)
|
||||
bottom.set(0f, center.y + b)
|
||||
} else if (degrees < 90f) {
|
||||
// Top right
|
||||
val phi = 90f - degrees
|
||||
val a = center.y
|
||||
val b = a * tan(phi.toRadians())
|
||||
top.set(center.x + b, 0f)
|
||||
bottom.set(center.x - b, rect.height())
|
||||
} else if (degrees < (90f + beta)) {
|
||||
// Top left
|
||||
val phi = degrees - 90f
|
||||
val a = center.y
|
||||
val b = a * tan(phi.toRadians())
|
||||
top.set(center.x - b, 0f)
|
||||
bottom.set(center.x + b, rect.height())
|
||||
} else if (degrees < 180f) {
|
||||
// left top
|
||||
val phi = 180f - degrees
|
||||
val a = center.x
|
||||
val b = a * tan(phi.toRadians())
|
||||
top.set(0f, center.y - b)
|
||||
bottom.set(rect.width(), center.y + b)
|
||||
} else if (degrees < (180f + alpha)) {
|
||||
// left bottom
|
||||
val phi = degrees - 180f
|
||||
val a = center.x
|
||||
val b = a * tan(phi.toRadians())
|
||||
top.set(0f, center.y + b)
|
||||
bottom.set(rect.width(), center.y - b)
|
||||
} else if (degrees < 270f) {
|
||||
// bottom left
|
||||
val phi = 270f - degrees
|
||||
val a = center.y
|
||||
val b = a * tan(phi.toRadians())
|
||||
top.set(center.x - b, rect.height())
|
||||
bottom.set(center.x + b, 0f)
|
||||
} else if (degrees < (270f + beta)) {
|
||||
// bottom right
|
||||
val phi = degrees - 270f
|
||||
val a = center.y
|
||||
val b = a * tan(phi.toRadians())
|
||||
top.set(center.x + b, rect.height())
|
||||
bottom.set(center.x - b, 0f)
|
||||
} else {
|
||||
// right bottom
|
||||
val phi = 360f - degrees
|
||||
val a = center.x
|
||||
val b = a * tan(phi.toRadians())
|
||||
top.set(rect.width(), center.y + b)
|
||||
bottom.set(0f, center.y - b)
|
||||
}
|
||||
|
||||
val (selected, other) = when (selectedEdge) {
|
||||
CustomChatColorEdge.TOP -> top to bottom
|
||||
CustomChatColorEdge.BOTTOM -> bottom to top
|
||||
}
|
||||
|
||||
val (selectedPaint, otherPaint) = when (selectedEdge) {
|
||||
CustomChatColorEdge.TOP -> topColorPaint to bottomColorPaint
|
||||
CustomChatColorEdge.BOTTOM -> bottomColorPaint to topColorPaint
|
||||
}
|
||||
|
||||
canvas.apply {
|
||||
save()
|
||||
translate(rect.top, rect.left)
|
||||
|
||||
drawLine(selected.x, selected.y, other.x, other.y, pipeBorderPaint)
|
||||
drawLine(selected.x, selected.y, other.x, other.y, pipePaint)
|
||||
|
||||
drawCircle(other.x, other.y, opaqueThumbRadius + thumbBorder, thumbBorderPaint)
|
||||
drawCircle(other.x, other.y, opaqueThumbRadius, backgroundPaint)
|
||||
drawCircle(other.x, other.y, opaqueThumbRadius - opaqueThumbPadding, otherPaint)
|
||||
|
||||
drawCircle(selected.x, selected.y, opaqueThumbRadius + thumbBorderSelected, thumbBorderPaintSelected)
|
||||
drawCircle(selected.x, selected.y, opaqueThumbRadius, backgroundPaint)
|
||||
drawCircle(selected.x, selected.y, opaqueThumbRadius - opaqueThumbPaddingSelected, selectedPaint)
|
||||
restore()
|
||||
}
|
||||
|
||||
top.offset(rect.top, rect.left)
|
||||
bottom.offset(rect.top, rect.left)
|
||||
}
|
||||
|
||||
private fun Float.toDegrees(): Float = this * (180f / Math.PI.toFloat())
|
||||
private fun Float.toRadians(): Float = this * (Math.PI.toFloat() / 180f)
|
||||
private fun PointF.distance(other: PointF): Float = abs(sqrt((this.x - other.x).pow(2) + (this.y - other.y).pow(2)))
|
||||
private fun PointF.dotProduct(other: PointF): Float = (this.x * other.x) + (this.y * other.y)
|
||||
private fun PointF.determinate(other: PointF): Float = (this.x * other.y) - (this.y * other.x)
|
||||
|
||||
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
var activePointerId: Int = MotionEvent.INVALID_POINTER_ID
|
||||
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
activePointerId = e.getPointerId(0)
|
||||
|
||||
val touchPoint = PointF(e.getX(activePointerId), e.getY(activePointerId))
|
||||
val distanceFromTop = touchPoint.distance(top)
|
||||
|
||||
if (distanceFromTop <= thumbRadius) {
|
||||
setSelectedEdge(CustomChatColorEdge.TOP)
|
||||
return true
|
||||
}
|
||||
|
||||
val distanceFromBottom = touchPoint.distance(bottom)
|
||||
if (distanceFromBottom <= thumbRadius) {
|
||||
setSelectedEdge(CustomChatColorEdge.BOTTOM)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
|
||||
val a = PointF(e2.getX(activePointerId) - center.x, e2.getY(activePointerId) - center.y)
|
||||
val b = PointF(center.x, 0f)
|
||||
val dot = a.dotProduct(b)
|
||||
val det = a.determinate(b)
|
||||
|
||||
val offset = if (selectedEdge == CustomChatColorEdge.BOTTOM) 180f else 0f
|
||||
val degrees = (atan2(det, dot).toDegrees() + 360f + offset) % 360f
|
||||
|
||||
setDegreesInternal(degrees)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun Float.rotate(degrees: Float): Float = (this + degrees + 360f) % 360f
|
||||
|
||||
interface Listener {
|
||||
fun onDegreesChanged(degrees: Float)
|
||||
fun onSelectedEdgeChanged(edge: CustomChatColorEdge)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private const val THUMB_RADIUS = 24
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private const val THUMB_BORDER = 1
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private const val THUMB_BORDER_SELECTED = 4
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private const val OPAQUE_THUMB_RADIUS = 20
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private const val OPAGUE_THUMB_PADDING = 2
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private const val OPAGUE_THUMB_PADDING_SELECTED = 1
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private const val PIPE_WIDTH = 6
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private const val PIPE_BORDER = 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package org.thoughtcrime.securesms.conversation.colors.ui.custom
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
|
||||
class CustomChatColorPagerAdapter(parentFragment: Fragment, private val arguments: Bundle) : FragmentStateAdapter(parentFragment) {
|
||||
override fun getItemCount(): Int = 2
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> CustomChatColorCreatorPageFragment.forSingle(arguments)
|
||||
1 -> CustomChatColorCreatorPageFragment.forGradient(arguments)
|
||||
else -> {
|
||||
throw AssertionError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -393,7 +393,7 @@ public final class ConversationListItem extends ConstraintLayout
|
|||
private void setRippleColor(Recipient recipient) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
((RippleDrawable)(getBackground()).mutate())
|
||||
.setColor(ColorStateList.valueOf(recipient.getColor().toConversationColor(getContext())));
|
||||
.setColor(ColorStateList.valueOf(recipient.getChatColors().asSingleColor()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.util.CursorUtil
|
||||
import org.thoughtcrime.securesms.util.SqlUtil
|
||||
|
||||
class ChatColorsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Database(context, databaseHelper) {
|
||||
|
||||
companion object {
|
||||
private const val TABLE_NAME = "chat_colors"
|
||||
private const val ID = "_id"
|
||||
private const val CHAT_COLORS = "chat_colors"
|
||||
|
||||
@JvmField
|
||||
val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
$CHAT_COLORS BLOB
|
||||
)
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
fun getById(chatColorsId: ChatColors.Id): ChatColors {
|
||||
val db = databaseHelper.readableDatabase
|
||||
val projection = arrayOf(ID, CHAT_COLORS)
|
||||
val args = SqlUtil.buildArgs(chatColorsId.longValue)
|
||||
|
||||
db.query(TABLE_NAME, projection, ID_WHERE, args, null, null, null)?.use {
|
||||
if (it.moveToFirst()) {
|
||||
return it.getChatColors()
|
||||
}
|
||||
}
|
||||
|
||||
throw IllegalArgumentException("Could not locate chat color $chatColorsId")
|
||||
}
|
||||
|
||||
fun saveChatColors(chatColors: ChatColors): ChatColors {
|
||||
return when (chatColors.id) {
|
||||
is ChatColors.Id.Auto -> throw AssertionError("Saving 'auto' does not make sense")
|
||||
is ChatColors.Id.BuiltIn -> chatColors
|
||||
is ChatColors.Id.NotSet -> insertChatColors(chatColors)
|
||||
is ChatColors.Id.Custom -> updateChatColors(chatColors)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSavedChatColors(): List<ChatColors> {
|
||||
val db = databaseHelper.readableDatabase
|
||||
val projection = arrayOf(ID, CHAT_COLORS)
|
||||
val result = mutableListOf<ChatColors>()
|
||||
|
||||
db.query(TABLE_NAME, projection, null, null, null, null, null)?.use {
|
||||
while (it.moveToNext()) {
|
||||
result.add(it.getChatColors())
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private fun insertChatColors(chatColors: ChatColors): ChatColors {
|
||||
if (chatColors.id != ChatColors.Id.NotSet) {
|
||||
throw IllegalArgumentException("Bad chat colors to insert.")
|
||||
}
|
||||
|
||||
val db: SQLiteDatabase = databaseHelper.writableDatabase
|
||||
val values = ContentValues(1).apply {
|
||||
put(CHAT_COLORS, chatColors.serialize().toByteArray())
|
||||
}
|
||||
|
||||
val rowId = db.insert(TABLE_NAME, null, values)
|
||||
if (rowId == -1L) {
|
||||
throw IllegalStateException("Failed to insert ChatColor into database")
|
||||
}
|
||||
|
||||
notifyListeners()
|
||||
|
||||
return chatColors.withId(ChatColors.Id.forLongValue(rowId))
|
||||
}
|
||||
|
||||
private fun updateChatColors(chatColors: ChatColors): ChatColors {
|
||||
if (chatColors.id == ChatColors.Id.NotSet || chatColors.id == ChatColors.Id.BuiltIn) {
|
||||
throw IllegalArgumentException("Bad chat colors to update.")
|
||||
}
|
||||
|
||||
val db: SQLiteDatabase = databaseHelper.writableDatabase
|
||||
val values = ContentValues(1).apply {
|
||||
put(CHAT_COLORS, chatColors.serialize().toByteArray())
|
||||
}
|
||||
|
||||
val rowsUpdated = db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(chatColors.id.longValue))
|
||||
if (rowsUpdated < 1) {
|
||||
throw IllegalStateException("Failed to update ChatColor in database")
|
||||
}
|
||||
|
||||
if (SignalStore.chatColorsValues().chatColors?.id == chatColors.id) {
|
||||
SignalStore.chatColorsValues().chatColors = chatColors
|
||||
}
|
||||
|
||||
val recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
recipientDatabase.onUpdatedChatColors(chatColors)
|
||||
notifyListeners()
|
||||
|
||||
return chatColors
|
||||
}
|
||||
|
||||
fun deleteChatColors(chatColors: ChatColors) {
|
||||
if (chatColors.id == ChatColors.Id.NotSet || chatColors.id == ChatColors.Id.BuiltIn) {
|
||||
throw IllegalArgumentException("Cannot delete this chat color")
|
||||
}
|
||||
|
||||
val db: SQLiteDatabase = databaseHelper.writableDatabase
|
||||
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(chatColors.id.longValue))
|
||||
|
||||
if (SignalStore.chatColorsValues().chatColors?.id == chatColors.id) {
|
||||
SignalStore.chatColorsValues().chatColors = null
|
||||
}
|
||||
|
||||
val recipientDatabase = DatabaseFactory.getRecipientDatabase(context)
|
||||
recipientDatabase.onDeletedChatColors(chatColors)
|
||||
notifyListeners()
|
||||
}
|
||||
|
||||
private fun notifyListeners() {
|
||||
ApplicationDependencies.getDatabaseObserver().notifyChatColorsListeners()
|
||||
}
|
||||
|
||||
private fun Cursor.getId(): Long = CursorUtil.requireLong(this, ID)
|
||||
private fun Cursor.getChatColors(): ChatColors = ChatColors.forChatColor(
|
||||
ChatColors.Id.forLongValue(getId()),
|
||||
ChatColor.parseFrom(CursorUtil.requireBlob(this, CHAT_COLORS))
|
||||
)
|
||||
}
|
|
@ -64,6 +64,7 @@ public class DatabaseFactory {
|
|||
private final RemappedRecordsDatabase remappedRecordsDatabase;
|
||||
private final MentionDatabase mentionDatabase;
|
||||
private final PaymentDatabase paymentDatabase;
|
||||
private final ChatColorsDatabase chatColorsDatabase;
|
||||
|
||||
public static DatabaseFactory getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
|
@ -174,6 +175,10 @@ public class DatabaseFactory {
|
|||
return getInstance(context).databaseHelper.getReadableDatabase().getSqlCipherDatabase();
|
||||
}
|
||||
|
||||
public static ChatColorsDatabase getChatColorsDatabase(Context context) {
|
||||
return getInstance(context).chatColorsDatabase;
|
||||
}
|
||||
|
||||
public static void upgradeRestored(Context context, SQLiteDatabase database){
|
||||
synchronized (lock) {
|
||||
getInstance(context).databaseHelper.onUpgrade(database, database.getVersion(), -1);
|
||||
|
@ -223,6 +228,7 @@ public class DatabaseFactory {
|
|||
this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper);
|
||||
this.mentionDatabase = new MentionDatabase(context, databaseHelper);
|
||||
this.paymentDatabase = new PaymentDatabase(context, databaseHelper);
|
||||
this.chatColorsDatabase = new ChatColorsDatabase(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,
|
||||
|
|
|
@ -29,6 +29,7 @@ public final class DatabaseObserver {
|
|||
private final Map<Long, Set<Observer>> verboseConversationObservers;
|
||||
private final Map<UUID, Set<Observer>> paymentObservers;
|
||||
private final Set<Observer> allPaymentsObservers;
|
||||
private final Set<Observer> chatColorsObservers;
|
||||
|
||||
public DatabaseObserver(Application application) {
|
||||
this.application = application;
|
||||
|
@ -38,6 +39,7 @@ public final class DatabaseObserver {
|
|||
this.verboseConversationObservers = new HashMap<>();
|
||||
this.paymentObservers = new HashMap<>();
|
||||
this.allPaymentsObservers = new HashSet<>();
|
||||
this.chatColorsObservers = new HashSet<>();
|
||||
}
|
||||
|
||||
public void registerConversationListObserver(@NonNull Observer listener) {
|
||||
|
@ -70,12 +72,19 @@ public final class DatabaseObserver {
|
|||
});
|
||||
}
|
||||
|
||||
public void registerChatColorsObserver(@NonNull Observer listener) {
|
||||
executor.execute(() -> {
|
||||
chatColorsObservers.add(listener);
|
||||
});
|
||||
}
|
||||
|
||||
public void unregisterObserver(@NonNull Observer listener) {
|
||||
executor.execute(() -> {
|
||||
conversationListObservers.remove(listener);
|
||||
unregisterMapped(conversationObservers, listener);
|
||||
unregisterMapped(verboseConversationObservers, listener);
|
||||
unregisterMapped(paymentObservers, listener);
|
||||
chatColorsObservers.remove(listener);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -131,6 +140,14 @@ public final class DatabaseObserver {
|
|||
});
|
||||
}
|
||||
|
||||
public void notifyChatColorsListeners() {
|
||||
executor.execute(() -> {
|
||||
for (Observer chatColorsObserver : chatColorsObservers) {
|
||||
chatColorsObserver.onChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private <K> void registerMapped(@NonNull Map<K, Set<Observer>> map, @NonNull K key, @NonNull Observer listener) {
|
||||
Set<Observer> listeners = map.get(key);
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.google.protobuf.InvalidProtocolBufferException;
|
|||
import net.sqlcipher.SQLException;
|
||||
import net.sqlcipher.database.SQLiteConstraintException;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.zkgroup.InvalidInputException;
|
||||
|
@ -23,12 +24,14 @@ import org.signal.zkgroup.groups.GroupMasterKey;
|
|||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileKeyCredentialColumnData;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras;
|
||||
|
@ -62,7 +65,6 @@ import org.whispersystems.libsignal.IdentityKey;
|
|||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
|
||||
|
@ -110,7 +112,7 @@ public class RecipientDatabase extends Database {
|
|||
private static final String CALL_VIBRATE = "call_vibrate";
|
||||
private static final String NOTIFICATION_CHANNEL = "notification_channel";
|
||||
private static final String MUTE_UNTIL = "mute_until";
|
||||
private static final String COLOR = "color";
|
||||
//private static final String COLOR = "color";
|
||||
private static final String SEEN_INVITE_REMINDER = "seen_invite_reminder";
|
||||
private static final String DEFAULT_SUBSCRIPTION_ID = "default_subscription_id";
|
||||
private static final String MESSAGE_EXPIRATION_TIME = "message_expiration_time";
|
||||
|
@ -145,6 +147,8 @@ public class RecipientDatabase extends Database {
|
|||
public static final String ABOUT_EMOJI = "about_emoji";
|
||||
private static final String EXTRAS = "extras";
|
||||
private static final String GROUPS_IN_COMMON = "groups_in_common";
|
||||
private static final String CHAT_COLORS = "chat_colors";
|
||||
private static final String CUSTOM_CHAT_COLORS_ID = "custom_chat_colors_id";
|
||||
|
||||
public static final String SEARCH_PROFILE_NAME = "search_signal_profile";
|
||||
private static final String SORT_NAME = "sort_name";
|
||||
|
@ -160,7 +164,7 @@ public class RecipientDatabase extends Database {
|
|||
|
||||
private static final String[] RECIPIENT_PROJECTION = new String[] {
|
||||
ID, UUID, USERNAME, PHONE, EMAIL, GROUP_ID, GROUP_TYPE,
|
||||
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
|
||||
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
|
||||
PROFILE_KEY, PROFILE_KEY_CREDENTIAL,
|
||||
SYSTEM_JOINED_NAME, SYSTEM_GIVEN_NAME, SYSTEM_FAMILY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_PHONE_TYPE, SYSTEM_CONTACT_URI,
|
||||
PROFILE_GIVEN_NAME, PROFILE_FAMILY_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, LAST_PROFILE_FETCH,
|
||||
|
@ -172,7 +176,8 @@ public class RecipientDatabase extends Database {
|
|||
MENTION_SETTING, WALLPAPER, WALLPAPER_URI,
|
||||
MENTION_SETTING,
|
||||
ABOUT, ABOUT_EMOJI,
|
||||
EXTRAS, GROUPS_IN_COMMON
|
||||
EXTRAS, GROUPS_IN_COMMON,
|
||||
CHAT_COLORS, CUSTOM_CHAT_COLORS_ID
|
||||
};
|
||||
|
||||
private static final String[] ID_PROJECTION = new String[]{ID};
|
||||
|
@ -321,7 +326,6 @@ public class RecipientDatabase extends Database {
|
|||
CALL_VIBRATE + " INTEGER DEFAULT " + VibrateState.DEFAULT.getId() + ", " +
|
||||
NOTIFICATION_CHANNEL + " TEXT DEFAULT NULL, " +
|
||||
MUTE_UNTIL + " INTEGER DEFAULT 0, " +
|
||||
COLOR + " TEXT DEFAULT NULL, " +
|
||||
SEEN_INVITE_REMINDER + " INTEGER DEFAULT " + InsightsBannerTier.NO_TIER.getId() + ", " +
|
||||
DEFAULT_SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " +
|
||||
MESSAGE_EXPIRATION_TIME + " INTEGER DEFAULT 0, " +
|
||||
|
@ -354,8 +358,10 @@ public class RecipientDatabase extends Database {
|
|||
WALLPAPER_URI + " TEXT DEFAULT NULL, " +
|
||||
ABOUT + " TEXT DEFAULT NULL, " +
|
||||
ABOUT_EMOJI + " TEXT DEFAULT NULL, " +
|
||||
EXTRAS + " BLOB DEFAULT NULL, " +
|
||||
GROUPS_IN_COMMON + " INTEGER DEFAULT 0);";
|
||||
EXTRAS + " BLOB DEFAULT NULL, " +
|
||||
GROUPS_IN_COMMON + " INTEGER DEFAULT 0, " +
|
||||
CHAT_COLORS + " BLOB DEFAULT NULL, " +
|
||||
CUSTOM_CHAT_COLORS_ID + " INTEGER DEFAULT 0);";
|
||||
|
||||
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
|
||||
" FROM " + TABLE_NAME +
|
||||
|
@ -1025,10 +1031,6 @@ public class RecipientDatabase extends Database {
|
|||
values.put(MUTE_UNTIL, contact.getMuteUntil());
|
||||
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.getId().getRaw()));
|
||||
|
||||
if (contact.isProfileSharingEnabled() && isInsert && !profileName.isEmpty()) {
|
||||
values.put(COLOR, ContactColors.generateFor(profileName.toString()).serialize());
|
||||
}
|
||||
|
||||
if (contact.hasUnknownFields()) {
|
||||
values.put(STORAGE_PROTO, Base64.encodeBytes(contact.serializeUnknownFields()));
|
||||
} else {
|
||||
|
@ -1170,7 +1172,6 @@ public class RecipientDatabase extends Database {
|
|||
int messageVibrateState = CursorUtil.requireInt(cursor, MESSAGE_VIBRATE);
|
||||
int callVibrateState = CursorUtil.requireInt(cursor, CALL_VIBRATE);
|
||||
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
|
||||
String serializedColor = CursorUtil.requireString(cursor, COLOR);
|
||||
int insightsBannerTier = CursorUtil.requireInt(cursor, SEEN_INVITE_REMINDER);
|
||||
int defaultSubscriptionId = CursorUtil.requireInt(cursor, DEFAULT_SUBSCRIPTION_ID);
|
||||
int expireMessages = CursorUtil.requireInt(cursor, MESSAGE_EXPIRATION_TIME);
|
||||
|
@ -1195,21 +1196,15 @@ public class RecipientDatabase extends Database {
|
|||
String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID);
|
||||
int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING);
|
||||
byte[] wallpaper = CursorUtil.requireBlob(cursor, WALLPAPER);
|
||||
byte[] serializedChatColors = CursorUtil.requireBlob(cursor, CHAT_COLORS);
|
||||
long customChatColorsId = CursorUtil.requireLong(cursor, CUSTOM_CHAT_COLORS_ID);
|
||||
String about = CursorUtil.requireString(cursor, ABOUT);
|
||||
String aboutEmoji = CursorUtil.requireString(cursor, ABOUT_EMOJI);
|
||||
boolean hasGroupsInCommon = CursorUtil.requireBoolean(cursor, GROUPS_IN_COMMON);
|
||||
|
||||
MaterialColor color;
|
||||
byte[] profileKey = null;
|
||||
ProfileKeyCredential profileKeyCredential = null;
|
||||
|
||||
try {
|
||||
color = serializedColor == null ? null : MaterialColor.fromSerialized(serializedColor);
|
||||
} catch (MaterialColor.UnknownColorException e) {
|
||||
Log.w(TAG, e);
|
||||
color = null;
|
||||
}
|
||||
|
||||
if (profileKeyString != null) {
|
||||
try {
|
||||
profileKey = Base64.decode(profileKeyString);
|
||||
|
@ -1247,6 +1242,15 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
ChatColors chatColors = null;
|
||||
if (serializedChatColors != null) {
|
||||
try {
|
||||
chatColors = ChatColors.forChatColor(ChatColors.Id.forLongValue(customChatColorsId), ChatColor.parseFrom(serializedChatColors));
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "Failed to parse chat colors.", e);
|
||||
}
|
||||
}
|
||||
|
||||
return new RecipientSettings(RecipientId.from(id),
|
||||
uuid,
|
||||
username,
|
||||
|
@ -1260,7 +1264,6 @@ public class RecipientDatabase extends Database {
|
|||
VibrateState.fromId(callVibrateState),
|
||||
Util.uri(messageRingtone),
|
||||
Util.uri(callRingtone),
|
||||
color,
|
||||
defaultSubscriptionId,
|
||||
expireMessages,
|
||||
RegisteredState.fromId(registeredState),
|
||||
|
@ -1284,6 +1287,7 @@ public class RecipientDatabase extends Database {
|
|||
storageKey,
|
||||
MentionSetting.fromId(mentionSettingId),
|
||||
chatWallpaper,
|
||||
chatColors,
|
||||
about,
|
||||
aboutEmoji,
|
||||
getSyncExtras(cursor),
|
||||
|
@ -1333,31 +1337,123 @@ public class RecipientDatabase extends Database {
|
|||
return new BulkOperationsHandle(database);
|
||||
}
|
||||
|
||||
public void setColor(@NonNull RecipientId id, @NonNull MaterialColor color) {
|
||||
void onUpdatedChatColors(@NonNull ChatColors chatColors) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
String where = CUSTOM_CHAT_COLORS_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(chatColors.getId().getLongValue());
|
||||
List<RecipientId> updated = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
updated.add(RecipientId.from(CursorUtil.requireLong(cursor, ID)));
|
||||
}
|
||||
}
|
||||
|
||||
if (updated.isEmpty()) {
|
||||
Log.d(TAG, "No recipients utilizing updated chat color.");
|
||||
} else {
|
||||
ContentValues values = new ContentValues(2);
|
||||
|
||||
values.put(CHAT_COLORS, chatColors.serialize().toByteArray());
|
||||
values.put(CUSTOM_CHAT_COLORS_ID, chatColors.getId().getLongValue());
|
||||
|
||||
database.update(TABLE_NAME, values, where, args);
|
||||
|
||||
for (RecipientId recipientId : updated) {
|
||||
Recipient.live(recipientId).refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onDeletedChatColors(@NonNull ChatColors chatColors) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
String where = CUSTOM_CHAT_COLORS_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(chatColors.getId().getLongValue());
|
||||
List<RecipientId> updated = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
updated.add(RecipientId.from(CursorUtil.requireLong(cursor, ID)));
|
||||
}
|
||||
}
|
||||
|
||||
if (updated.isEmpty()) {
|
||||
Log.d(TAG, "No recipients utilizing deleted chat color.");
|
||||
} else {
|
||||
ContentValues values = new ContentValues(2);
|
||||
|
||||
values.put(CHAT_COLORS, (byte[]) null);
|
||||
values.put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.INSTANCE.getLongValue());
|
||||
|
||||
database.update(TABLE_NAME, values, where, args);
|
||||
|
||||
for (RecipientId recipientId : updated) {
|
||||
Recipient.live(recipientId).refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getColorUsageCount(@NotNull ChatColors chatColors) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] projection = SqlUtil.buildArgs("COUNT(*)");
|
||||
String where = CUSTOM_CHAT_COLORS_ID + " = ?";
|
||||
String[] args = SqlUtil.buildArgs(chatColors.getId().getLongValue());
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, projection, where, args, null, null, null)) {
|
||||
if (cursor == null) {
|
||||
return 0;
|
||||
} else {
|
||||
cursor.moveToFirst();
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clearAllColors() {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
String where = CUSTOM_CHAT_COLORS_ID + " != ?";
|
||||
String[] args = SqlUtil.buildArgs(ChatColors.Id.NotSet.INSTANCE.getLongValue());
|
||||
List<RecipientId> toUpdate = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = database.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
toUpdate.add(RecipientId.from(CursorUtil.requireLong(cursor, ID)));
|
||||
}
|
||||
}
|
||||
|
||||
if (toUpdate.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLOR, color.serialize());
|
||||
values.put(CHAT_COLORS, (byte[]) null);
|
||||
values.put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.INSTANCE.getLongValue());
|
||||
|
||||
database.update(TABLE_NAME, values, where, args);
|
||||
|
||||
for (RecipientId id : toUpdate) {
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void clearColor(@NonNull RecipientId id) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(CHAT_COLORS, (byte[]) null);
|
||||
values.put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.INSTANCE.getLongValue());
|
||||
if (update(id, values)) {
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public void setColorIfNotSet(@NonNull RecipientId id, @NonNull MaterialColor color) {
|
||||
if (setColorIfNotSetInternal(id, color)) {
|
||||
public void setColor(@NonNull RecipientId id, @NonNull ChatColors color) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(CHAT_COLORS, color.serialize().toByteArray());
|
||||
values.put(CUSTOM_CHAT_COLORS_ID, color.getId().getLongValue());
|
||||
if (update(id, values)) {
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean setColorIfNotSetInternal(@NonNull RecipientId id, @NonNull MaterialColor color) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
String query = ID + " = ? AND " + COLOR + " IS NULL";
|
||||
String[] args = new String[]{ id.serialize() };
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLOR, color.serialize());
|
||||
|
||||
return db.update(TABLE_NAME, values, query, args) > 0;
|
||||
}
|
||||
|
||||
public void setDefaultSubscriptionId(@NonNull RecipientId id, int defaultSubscriptionId) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(DEFAULT_SUBSCRIPTION_ID, defaultSubscriptionId);
|
||||
|
@ -1739,7 +1835,6 @@ public class RecipientDatabase extends Database {
|
|||
contentValues.put(PROFILE_SHARING, enabled ? 1 : 0);
|
||||
|
||||
boolean profiledUpdated = update(id, contentValues);
|
||||
boolean colorUpdated = enabled && setColorIfNotSetInternal(id, ContactColors.generateFor(Recipient.resolved(id).getDisplayName(context)));
|
||||
|
||||
if (profiledUpdated && enabled) {
|
||||
Optional<GroupDatabase.GroupRecord> group = DatabaseFactory.getGroupDatabase(context).getGroup(id);
|
||||
|
@ -1749,7 +1844,7 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
if (profiledUpdated || colorUpdated) {
|
||||
if (profiledUpdated) {
|
||||
rotateStorageId(id);
|
||||
Recipient.live(id).refresh();
|
||||
StorageSyncHelper.scheduleSyncForDataChange();
|
||||
|
@ -2161,22 +2256,55 @@ public class RecipientDatabase extends Database {
|
|||
return results;
|
||||
}
|
||||
|
||||
public void updateSystemContactColors(@NonNull ColorUpdater updater) {
|
||||
/**
|
||||
* We no longer automatically generate a chat color. This method is used only
|
||||
* in the case of a legacy migration and otherwise should not be called.
|
||||
*/
|
||||
@Deprecated
|
||||
public void updateSystemContactColors() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Map<RecipientId, MaterialColor> updates = new HashMap<>();
|
||||
Map<RecipientId, ChatColors> updates = new HashMap<>();
|
||||
|
||||
db.beginTransaction();
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] {ID, COLOR, SYSTEM_JOINED_NAME}, SYSTEM_JOINED_NAME + " IS NOT NULL AND " + SYSTEM_JOINED_NAME + " != \"\"", null, null, null, null)) {
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[] {ID, "color", CHAT_COLORS, CUSTOM_CHAT_COLORS_ID, SYSTEM_JOINED_NAME}, SYSTEM_JOINED_NAME + " IS NOT NULL AND " + SYSTEM_JOINED_NAME + " != \"\"", null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
long id = cursor.getLong(cursor.getColumnIndexOrThrow(ID));
|
||||
MaterialColor newColor = updater.update(cursor.getString(cursor.getColumnIndexOrThrow(SYSTEM_JOINED_NAME)),
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(COLOR)));
|
||||
long id = CursorUtil.requireLong(cursor, ID);
|
||||
String serializedColor = CursorUtil.requireString(cursor, "color");
|
||||
long customChatColorsId = CursorUtil.requireLong(cursor, CUSTOM_CHAT_COLORS_ID);
|
||||
byte[] serializedChatColors = CursorUtil.requireBlob(cursor, CHAT_COLORS);
|
||||
|
||||
ChatColors chatColors;
|
||||
if (serializedChatColors != null) {
|
||||
try {
|
||||
chatColors = ChatColors.forChatColor(ChatColors.Id.forLongValue(customChatColorsId), ChatColor.parseFrom(serializedChatColors));
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
chatColors = null;
|
||||
}
|
||||
} else {
|
||||
chatColors = null;
|
||||
}
|
||||
|
||||
if (chatColors != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (serializedColor != null) {
|
||||
try {
|
||||
chatColors = ChatColorsMapper.getChatColors(MaterialColor.fromSerialized(serializedColor));
|
||||
} catch (MaterialColor.UnknownColorException e) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(COLOR, newColor.serialize());
|
||||
contentValues.put(CHAT_COLORS, chatColors.serialize().toByteArray());
|
||||
contentValues.put(CUSTOM_CHAT_COLORS_ID, chatColors.getId().getLongValue());
|
||||
|
||||
db.update(TABLE_NAME, contentValues, ID + " = ?", new String[] { String.valueOf(id) });
|
||||
|
||||
updates.put(RecipientId.from(id), newColor);
|
||||
updates.put(RecipientId.from(id), chatColors);
|
||||
}
|
||||
} finally {
|
||||
db.setTransactionSuccessful();
|
||||
|
@ -2754,7 +2882,8 @@ public class RecipientDatabase extends Database {
|
|||
uuidValues.put(CALL_VIBRATE, uuidSettings.getCallVibrateState() != VibrateState.DEFAULT ? uuidSettings.getCallVibrateState().getId() : e164Settings.getCallVibrateState().getId());
|
||||
uuidValues.put(NOTIFICATION_CHANNEL, uuidSettings.getNotificationChannel() != null ? uuidSettings.getNotificationChannel() : e164Settings.getNotificationChannel());
|
||||
uuidValues.put(MUTE_UNTIL, uuidSettings.getMuteUntil() > 0 ? uuidSettings.getMuteUntil() : e164Settings.getMuteUntil());
|
||||
uuidValues.put(COLOR, Optional.fromNullable(uuidSettings.getColor()).or(Optional.fromNullable(e164Settings.getColor())).transform(MaterialColor::serialize).orNull());
|
||||
uuidValues.put(CHAT_COLORS, Optional.fromNullable(uuidSettings.getChatColors()).or(Optional.fromNullable(e164Settings.getChatColors())).transform(colors -> colors.serialize().toByteArray()).orNull());
|
||||
uuidValues.put(CUSTOM_CHAT_COLORS_ID, Optional.fromNullable(uuidSettings.getChatColors()).or(Optional.fromNullable(e164Settings.getChatColors())).transform(colors -> colors.getId().getLongValue()).orNull());
|
||||
uuidValues.put(SEEN_INVITE_REMINDER, e164Settings.getInsightsBannerTier().getId());
|
||||
uuidValues.put(DEFAULT_SUBSCRIPTION_ID, e164Settings.getDefaultSubscriptionId().or(-1));
|
||||
uuidValues.put(MESSAGE_EXPIRATION_TIME, uuidSettings.getExpireMessages() > 0 ? uuidSettings.getExpireMessages() : e164Settings.getExpireMessages());
|
||||
|
@ -2891,9 +3020,8 @@ public class RecipientDatabase extends Database {
|
|||
refreshQualifyingValues.put(SYSTEM_CONTACT_URI, systemContactUri);
|
||||
|
||||
boolean updatedValues = update(id, refreshQualifyingValues);
|
||||
boolean updatedColor = !TextUtils.isEmpty(joinedName) && setColorIfNotSetInternal(id, ContactColors.generateFor(joinedName));
|
||||
|
||||
if (updatedValues || updatedColor) {
|
||||
if (updatedValues) {
|
||||
pendingContactInfoMap.put(id, new PendingContactInfo(systemProfileName, photoUri, systemPhoneLabel, systemContactUri));
|
||||
}
|
||||
|
||||
|
@ -2951,7 +3079,7 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
|
||||
public interface ColorUpdater {
|
||||
MaterialColor update(@NonNull String name, @Nullable String color);
|
||||
ChatColors update(@NonNull String name, @Nullable MaterialColor materialColor);
|
||||
}
|
||||
|
||||
public static class RecipientSettings {
|
||||
|
@ -2968,7 +3096,6 @@ public class RecipientDatabase extends Database {
|
|||
private final VibrateState callVibrateState;
|
||||
private final Uri messageRingtone;
|
||||
private final Uri callRingtone;
|
||||
private final MaterialColor color;
|
||||
private final int defaultSubscriptionId;
|
||||
private final int expireMessages;
|
||||
private final RegisteredState registered;
|
||||
|
@ -2994,6 +3121,7 @@ public class RecipientDatabase extends Database {
|
|||
private final byte[] storageId;
|
||||
private final MentionSetting mentionSetting;
|
||||
private final ChatWallpaper wallpaper;
|
||||
private final ChatColors chatColors;
|
||||
private final String about;
|
||||
private final String aboutEmoji;
|
||||
private final SyncExtras syncExtras;
|
||||
|
@ -3013,7 +3141,6 @@ public class RecipientDatabase extends Database {
|
|||
@NonNull VibrateState callVibrateState,
|
||||
@Nullable Uri messageRingtone,
|
||||
@Nullable Uri callRingtone,
|
||||
@Nullable MaterialColor color,
|
||||
int defaultSubscriptionId,
|
||||
int expireMessages,
|
||||
@NonNull RegisteredState registered,
|
||||
|
@ -3037,6 +3164,7 @@ public class RecipientDatabase extends Database {
|
|||
@Nullable byte[] storageId,
|
||||
@NonNull MentionSetting mentionSetting,
|
||||
@Nullable ChatWallpaper wallpaper,
|
||||
@Nullable ChatColors chatColors,
|
||||
@Nullable String about,
|
||||
@Nullable String aboutEmoji,
|
||||
@NonNull SyncExtras syncExtras,
|
||||
|
@ -3056,7 +3184,6 @@ public class RecipientDatabase extends Database {
|
|||
this.callVibrateState = callVibrateState;
|
||||
this.messageRingtone = messageRingtone;
|
||||
this.callRingtone = callRingtone;
|
||||
this.color = color;
|
||||
this.defaultSubscriptionId = defaultSubscriptionId;
|
||||
this.expireMessages = expireMessages;
|
||||
this.registered = registered;
|
||||
|
@ -3082,6 +3209,7 @@ public class RecipientDatabase extends Database {
|
|||
this.storageId = storageId;
|
||||
this.mentionSetting = mentionSetting;
|
||||
this.wallpaper = wallpaper;
|
||||
this.chatColors = chatColors;
|
||||
this.about = about;
|
||||
this.aboutEmoji = aboutEmoji;
|
||||
this.syncExtras = syncExtras;
|
||||
|
@ -3117,10 +3245,6 @@ public class RecipientDatabase extends Database {
|
|||
return groupType;
|
||||
}
|
||||
|
||||
public @Nullable MaterialColor getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public boolean isBlocked() {
|
||||
return blocked;
|
||||
}
|
||||
|
@ -3241,6 +3365,10 @@ public class RecipientDatabase extends Database {
|
|||
return wallpaper;
|
||||
}
|
||||
|
||||
public @Nullable ChatColors getChatColors() {
|
||||
return chatColors;
|
||||
}
|
||||
|
||||
public @Nullable String getAbout() {
|
||||
return about;
|
||||
}
|
||||
|
|
|
@ -22,10 +22,14 @@ import net.sqlcipher.database.SQLiteDatabase;
|
|||
import net.sqlcipher.database.SQLiteOpenHelper;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper;
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.ChatColorsDatabase;
|
||||
import org.thoughtcrime.securesms.database.DraftDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||
|
@ -77,6 +81,7 @@ import java.util.HashSet;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatabase {
|
||||
|
@ -182,8 +187,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
|||
private static final int STORAGE_SERVICE_REFACTOR = 97;
|
||||
private static final int CLEAR_MMS_STORAGE_IDS = 98;
|
||||
private static final int SERVER_GUID = 99;
|
||||
private static final int CHAT_COLORS = 100;
|
||||
|
||||
private static final int DATABASE_VERSION = 99;
|
||||
private static final int DATABASE_VERSION = 100;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
|
@ -215,6 +221,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
|||
db.execSQL(UnknownStorageIdDatabase.CREATE_TABLE);
|
||||
db.execSQL(MentionDatabase.CREATE_TABLE);
|
||||
db.execSQL(PaymentDatabase.CREATE_TABLE);
|
||||
db.execSQL(ChatColorsDatabase.CREATE_TABLE);
|
||||
executeStatements(db, SearchDatabase.CREATE_TABLE);
|
||||
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE);
|
||||
|
||||
|
@ -1463,6 +1470,27 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
|
|||
db.execSQL("ALTER TABLE mms ADD COLUMN server_guid TEXT DEFAULT NULL");
|
||||
}
|
||||
|
||||
if (oldVersion < CHAT_COLORS) {
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN chat_colors BLOB DEFAULT NULL");
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN custom_chat_colors_id INTEGER DEFAULT 0");
|
||||
db.execSQL("CREATE TABLE chat_colors (" +
|
||||
"_id INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||
"chat_colors BLOB)");
|
||||
|
||||
Set<Map.Entry<MaterialColor, ChatColors>> entrySet = ChatColorsMapper.getEntrySet();
|
||||
String where = "color = ? AND group_id is NULL";
|
||||
|
||||
for (Map.Entry<MaterialColor, ChatColors> entry : entrySet) {
|
||||
String[] whereArgs = SqlUtil.buildArgs(entry.getKey().serialize());
|
||||
ContentValues values = new ContentValues(2);
|
||||
|
||||
values.put("chat_colors", entry.getValue().serialize().toByteArray());
|
||||
values.put("custom_chat_colors_id", entry.getValue().getId().getLongValue());
|
||||
|
||||
db.update("recipient", values, where, whereArgs);
|
||||
}
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
|
||||
public interface GiphyMp4Playable {
|
||||
/**
|
||||
* Shows the area in which a video would be projected. Called when a video will not
|
||||
|
@ -40,8 +43,9 @@ public interface GiphyMp4Playable {
|
|||
|
||||
/**
|
||||
* Width, height, and (x,y) of view which video player will "project" into
|
||||
* @param viewGroup
|
||||
*/
|
||||
@NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerview);
|
||||
@NonNull Projection getProjection(@NonNull ViewGroup viewGroup);
|
||||
|
||||
/**
|
||||
* Specifies whether the content can start playing.
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
package org.thoughtcrime.securesms.giph.mp4;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
import android.view.ViewParent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.components.CornerMask;
|
||||
|
||||
/**
|
||||
* Describes the position and size of the area where a video should play.
|
||||
*/
|
||||
public final class GiphyMp4Projection {
|
||||
|
||||
private final float x;
|
||||
private final float y;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final CornerMask cornerMask;
|
||||
|
||||
public GiphyMp4Projection(float x, float y, int width, int height, @Nullable CornerMask cornerMask) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.cornerMask = cornerMask;
|
||||
}
|
||||
|
||||
public float getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public float getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public @Nullable CornerMask getCornerMask() {
|
||||
return cornerMask;
|
||||
}
|
||||
|
||||
public @NonNull GiphyMp4Projection translateX(float xTranslation) {
|
||||
return new GiphyMp4Projection(x + xTranslation, y, width, height, cornerMask);
|
||||
}
|
||||
|
||||
public static @NonNull GiphyMp4Projection forView(@NonNull RecyclerView recyclerView, @NonNull View view, @Nullable CornerMask cornerMask) {
|
||||
Rect viewBounds = new Rect();
|
||||
|
||||
view.getDrawingRect(viewBounds);
|
||||
recyclerView.offsetDescendantRectToMyCoords(view, viewBounds);
|
||||
return new GiphyMp4Projection(viewBounds.left, viewBounds.top, view.getWidth(), view.getHeight(), cornerMask);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
|||
import org.signal.glide.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.CornerMask;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -120,7 +121,7 @@ public final class GiphyMp4ProjectionPlayerHolder implements Player.EventListene
|
|||
return holders;
|
||||
}
|
||||
|
||||
public void setCornerMask(@Nullable CornerMask cornerMask) {
|
||||
player.setCornerMask(cornerMask);
|
||||
public void setCorners(@Nullable Projection.Corners corners) {
|
||||
player.setCorners(corners);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,9 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemSwipeCallback;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
@ -16,7 +19,7 @@ import java.util.Set;
|
|||
/**
|
||||
* Logic for updating content and positioning of videos as the user scrolls the list of gifs.
|
||||
*/
|
||||
public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackController.Callback, GiphyMp4DisplayUpdater {
|
||||
public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackController.Callback {
|
||||
|
||||
private final List<GiphyMp4ProjectionPlayerHolder> holders;
|
||||
private final SparseArray<GiphyMp4ProjectionPlayerHolder> playing;
|
||||
|
@ -48,7 +51,6 @@ public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackControl
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDisplay(@NonNull RecyclerView recyclerView, @NonNull GiphyMp4Playable holder) {
|
||||
GiphyMp4ProjectionPlayerHolder playerHolder = getCurrentHolder(holder.getAdapterPosition());
|
||||
if (playerHolder != null) {
|
||||
|
@ -84,7 +86,7 @@ public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackControl
|
|||
}
|
||||
|
||||
private void updateDisplay(@NonNull RecyclerView recyclerView, @NonNull GiphyMp4ProjectionPlayerHolder holder, @NonNull GiphyMp4Playable giphyMp4Playable) {
|
||||
GiphyMp4Projection projection = giphyMp4Playable.getProjection(recyclerView);
|
||||
Projection projection = giphyMp4Playable.getProjection(recyclerView);
|
||||
|
||||
holder.getContainer().setX(projection.getX());
|
||||
holder.getContainer().setY(projection.getY());
|
||||
|
@ -96,7 +98,7 @@ public final class GiphyMp4ProjectionRecycler implements GiphyMp4PlaybackControl
|
|||
holder.getContainer().setLayoutParams(params);
|
||||
}
|
||||
|
||||
holder.setCornerMask(projection.getCornerMask());
|
||||
holder.setCorners(projection.getCorners());
|
||||
}
|
||||
|
||||
private void startPlayback(@NonNull GiphyMp4ProjectionPlayerHolder holder, @NonNull GiphyMp4Playable giphyMp4Playable) {
|
||||
|
|
|
@ -20,6 +20,7 @@ import com.google.android.exoplayer2.ui.PlayerView;
|
|||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.CornerMask;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
|
||||
/**
|
||||
* Video Player class specifically created for the GiphyMp4Fragment.
|
||||
|
@ -72,8 +73,13 @@ public final class GiphyMp4VideoPlayer extends FrameLayout implements DefaultLif
|
|||
exoPlayer.prepare(mediaSource);
|
||||
}
|
||||
|
||||
void setCornerMask(@Nullable CornerMask cornerMask) {
|
||||
this.cornerMask = new CornerMask(this, cornerMask);
|
||||
void setCorners(@Nullable Projection.Corners corners) {
|
||||
if (corners == null) {
|
||||
this.cornerMask = null;
|
||||
} else {
|
||||
this.cornerMask = new CornerMask(this);
|
||||
this.cornerMask.setRadii(corners.getTopLeft(), corners.getTopRight(), corners.getBottomRight(), corners.getBottomLeft());
|
||||
}
|
||||
invalidate();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.graphics.drawable.ColorDrawable;
|
|||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -16,10 +17,11 @@ import com.google.android.exoplayer2.source.MediaSource;
|
|||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
|
||||
import org.thoughtcrime.securesms.giph.model.GiphyImage;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
/**
|
||||
|
@ -44,7 +46,7 @@ final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder implements GiphyM
|
|||
this.container = (AspectRatioFrameLayout) itemView;
|
||||
this.listener = listener;
|
||||
this.stillImage = itemView.findViewById(R.id.still_image);
|
||||
this.placeholder = new ColorDrawable(Util.getRandomElement(MaterialColor.values()).toConversationColor(itemView.getContext()));
|
||||
this.placeholder = new ColorDrawable(Util.getRandomElement(ChatColorsPalette.Names.getAll()).getColor(itemView.getContext()));
|
||||
this.mediaSourceFactory = mediaSourceFactory;
|
||||
|
||||
container.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH);
|
||||
|
@ -78,8 +80,8 @@ final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder implements GiphyM
|
|||
}
|
||||
|
||||
@Override
|
||||
public @NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerView) {
|
||||
return GiphyMp4Projection.forView(recyclerView, itemView, null);
|
||||
public @NonNull Projection getProjection(@NonNull ViewGroup recyclerView) {
|
||||
return Projection.relativeToParent(recyclerView, itemView, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -21,12 +21,12 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
|||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil;
|
||||
|
@ -88,7 +88,7 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
|
||||
groupCancelButton.setOnClickListener(v -> dismiss());
|
||||
|
||||
avatar.setImageBytesForGroup(null, new FallbackPhotoProvider(), MaterialColor.STEEL);
|
||||
avatar.setImageBytesForGroup(null, new FallbackPhotoProvider(), ChatColorsPalette.UNKNOWN_CONTACT);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
@ -130,7 +130,7 @@ public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
break;
|
||||
}
|
||||
|
||||
avatar.setImageBytesForGroup(details.getAvatarBytes(), new FallbackPhotoProvider(), MaterialColor.STEEL);
|
||||
avatar.setImageBytesForGroup(details.getAvatarBytes(), new FallbackPhotoProvider(), ChatColorsPalette.UNKNOWN_CONTACT);
|
||||
|
||||
groupCancelButton.setVisibility(View.VISIBLE);
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -18,6 +19,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
|
@ -32,12 +34,13 @@ import org.thoughtcrime.securesms.MediaPreviewActivity;
|
|||
import org.thoughtcrime.securesms.MuteDialog;
|
||||
import org.thoughtcrime.securesms.PushContactSelectionActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||
|
@ -122,14 +125,7 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
private View toggleAllMembers;
|
||||
private View groupLinkRow;
|
||||
private TextView groupLinkButton;
|
||||
private View wallpaperButton;
|
||||
|
||||
private final Recipient.FallbackPhotoProvider fallbackPhotoProvider = new Recipient.FallbackPhotoProvider() {
|
||||
@Override
|
||||
public @NonNull FallbackContactPhoto getPhotoForGroup() {
|
||||
return new FallbackPhoto80dp(R.drawable.ic_group_80, MaterialColor.ULTRAMARINE.toAvatarColor(requireContext()));
|
||||
}
|
||||
};
|
||||
private TextView wallpaperButton;
|
||||
|
||||
static ManageGroupFragment newInstance(@NonNull String groupId) {
|
||||
ManageGroupFragment fragment = new ManageGroupFragment();
|
||||
|
@ -227,8 +223,6 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
}
|
||||
});
|
||||
|
||||
avatar.setFallbackPhotoProvider(fallbackPhotoProvider);
|
||||
|
||||
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
|
||||
toolbar.setOnMenuItemClickListener(this::onMenuItemSelected);
|
||||
toolbar.inflateMenu(R.menu.manage_group_fragment);
|
||||
|
@ -244,6 +238,7 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
viewModel.getMemberCountSummary().observe(getViewLifecycleOwner(), memberCountUnderAvatar::setText);
|
||||
viewModel.getFullMemberCountSummary().observe(getViewLifecycleOwner(), memberCountAboveList::setText);
|
||||
viewModel.getGroupRecipient().observe(getViewLifecycleOwner(), groupRecipient -> {
|
||||
avatar.setFallbackPhotoProvider(new FallbackPhotoProvider(groupRecipient.getChatColors()));
|
||||
avatar.setRecipient(groupRecipient);
|
||||
avatar.setOnClickListener(v -> {
|
||||
FragmentActivity activity = requireActivity();
|
||||
|
@ -253,6 +248,10 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
customNotificationsRow.setOnClickListener(v -> CustomNotificationsDialogFragment.create(groupRecipient.getId())
|
||||
.show(requireFragmentManager(), DIALOG_TAG));
|
||||
wallpaperButton.setOnClickListener(v -> startActivity(ChatWallpaperActivity.createIntent(requireContext(), groupRecipient.getId())));
|
||||
|
||||
Drawable colorCircle = groupRecipient.getChatColors().asCircle();
|
||||
colorCircle.setBounds(0, 0, ViewUtil.dpToPx(16), ViewUtil.dpToPx(16));
|
||||
TextViewCompat.setCompoundDrawablesRelative(wallpaperButton, null, null, colorCircle, null);
|
||||
});
|
||||
|
||||
if (groupId.isV2()) {
|
||||
|
@ -506,4 +505,18 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
private final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
|
||||
|
||||
private final ChatColors groupColors;
|
||||
|
||||
private FallbackPhotoProvider(@NonNull ChatColors groupColors) {
|
||||
this.groupColors = groupColors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull FallbackContactPhoto getPhotoForGroup() {
|
||||
return new FallbackPhoto80dp(R.drawable.ic_group_80, groupColors);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,10 +9,10 @@ import androidx.core.util.Consumer;
|
|||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
|
@ -67,11 +67,7 @@ public class InsightsRepository implements InsightsDashboardViewModel.Repository
|
|||
SimpleTask.run(() -> {
|
||||
Recipient self = Recipient.self().resolve();
|
||||
String name = Optional.fromNullable(self.getDisplayName(context)).or("");
|
||||
MaterialColor fallbackColor = self.getColor();
|
||||
|
||||
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
|
||||
fallbackColor = ContactColors.generateFor(name);
|
||||
}
|
||||
ChatColors fallbackColor = self.getChatColors();
|
||||
|
||||
return new InsightsUserAvatar(new ProfileContactPhoto(self, self.getProfileAvatar()),
|
||||
fallbackColor,
|
||||
|
|
|
@ -8,24 +8,24 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
|
||||
class InsightsUserAvatar {
|
||||
private final ProfileContactPhoto profileContactPhoto;
|
||||
private final MaterialColor fallbackColor;
|
||||
private final ChatColors fallbackColor;
|
||||
private final FallbackContactPhoto fallbackContactPhoto;
|
||||
|
||||
InsightsUserAvatar(@NonNull ProfileContactPhoto profileContactPhoto, @NonNull MaterialColor fallbackColor, @NonNull FallbackContactPhoto fallbackContactPhoto) {
|
||||
InsightsUserAvatar(@NonNull ProfileContactPhoto profileContactPhoto, @NonNull ChatColors fallbackColor, @NonNull FallbackContactPhoto fallbackContactPhoto) {
|
||||
this.profileContactPhoto = profileContactPhoto;
|
||||
this.fallbackColor = fallbackColor;
|
||||
this.fallbackContactPhoto = fallbackContactPhoto;
|
||||
}
|
||||
|
||||
private Drawable fallbackDrawable(@NonNull Context context) {
|
||||
return fallbackContactPhoto.asDrawable(context, fallbackColor.toAvatarColor(context));
|
||||
return fallbackContactPhoto.asDrawable(context, fallbackColor);
|
||||
}
|
||||
|
||||
void load(ImageView into) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -144,7 +145,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
out.write(new DeviceContact(RecipientUtil.toSignalServiceAddress(context, recipient),
|
||||
Optional.fromNullable(recipient.isGroup() || recipient.isSystemContact() ? recipient.getDisplayName(context) : null),
|
||||
getAvatar(recipient.getId(), recipient.getContactUri()),
|
||||
Optional.fromNullable(recipient.getColor().serialize()),
|
||||
Optional.of(ChatColorsMapper.getMaterialColor(recipient.getChatColors()).serialize()),
|
||||
verifiedMessage,
|
||||
ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey()),
|
||||
recipient.isBlocked(),
|
||||
|
@ -198,7 +199,6 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
Optional<IdentityDatabase.IdentityRecord> identity = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipient.getId());
|
||||
Optional<VerifiedMessage> verified = getVerifiedMessage(recipient, identity);
|
||||
Optional<String> name = Optional.fromNullable(recipient.isSystemContact() ? recipient.getDisplayName(context) : recipient.getGroupName(context));
|
||||
Optional<String> color = Optional.of(recipient.getColor().serialize());
|
||||
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
|
||||
boolean blocked = recipient.isBlocked();
|
||||
Optional<Integer> expireTimer = recipient.getExpireMessages() > 0 ? Optional.of(recipient.getExpireMessages()) : Optional.absent();
|
||||
|
@ -207,7 +207,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
out.write(new DeviceContact(RecipientUtil.toSignalServiceAddress(context, recipient),
|
||||
name,
|
||||
getAvatar(recipient.getId(), recipient.getContactUri()),
|
||||
color,
|
||||
Optional.of(ChatColorsMapper.getMaterialColor(recipient.getChatColors()).serialize()),
|
||||
verified,
|
||||
profileKey,
|
||||
blocked,
|
||||
|
@ -224,7 +224,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
|
|||
out.write(new DeviceContact(RecipientUtil.toSignalServiceAddress(context, self),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.of(self.getColor().serialize()),
|
||||
Optional.of(ChatColorsMapper.getMaterialColor(self.getChatColors()).serialize()),
|
||||
Optional.absent(),
|
||||
ProfileKeyUtil.profileKeyOptionalOrThrow(self.getProfileKey()),
|
||||
false,
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.os.ParcelFileDescriptor;
|
|||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper;
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
|
@ -117,7 +118,7 @@ public class MultiDeviceGroupUpdateJob extends BaseJob {
|
|||
getAvatar(record.getRecipientId()),
|
||||
record.isActive(),
|
||||
expirationTimer,
|
||||
Optional.of(recipient.getColor().serialize()),
|
||||
Optional.of(ChatColorsMapper.getMaterialColor(recipient.getChatColors()).serialize()),
|
||||
recipient.isBlocked(),
|
||||
Optional.fromNullable(inboxPositions.get(recipientId)),
|
||||
archived.contains(recipientId)));
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package org.thoughtcrime.securesms.keyvalue
|
||||
|
||||
import com.google.protobuf.InvalidProtocolBufferException
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor
|
||||
|
||||
internal class ChatColorsValues internal constructor(store: KeyValueStore) : SignalStoreValues(store) {
|
||||
|
||||
companion object {
|
||||
private const val KEY_CHAT_COLORS = "chat_colors.chat_colors"
|
||||
private const val KEY_CHAT_COLORS_ID = "chat_colors.chat_colors.id"
|
||||
}
|
||||
|
||||
override fun onFirstEverAppLaunch() = Unit
|
||||
|
||||
override fun getKeysToIncludeInBackup(): MutableList<String> = mutableListOf()
|
||||
|
||||
val hasChatColors: Boolean
|
||||
@JvmName("hasChatColors")
|
||||
get() = chatColors != null
|
||||
|
||||
var chatColors: ChatColors?
|
||||
get() = getBlob(KEY_CHAT_COLORS, null)?.let { bytes ->
|
||||
try {
|
||||
ChatColors.forChatColor(chatColorsId, ChatColor.parseFrom(bytes))
|
||||
} catch (e: InvalidProtocolBufferException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
set(value) {
|
||||
if (value != null) {
|
||||
putBlob(KEY_CHAT_COLORS, value.serialize().toByteArray())
|
||||
chatColorsId = value.id
|
||||
} else {
|
||||
remove(KEY_CHAT_COLORS)
|
||||
}
|
||||
}
|
||||
|
||||
private var chatColorsId: ChatColors.Id
|
||||
get() = ChatColors.Id.forLongValue(getLong(KEY_CHAT_COLORS_ID, ChatColors.Id.NotSet.longValue))
|
||||
set(value) = putLong(KEY_CHAT_COLORS_ID, value.longValue)
|
||||
}
|
|
@ -36,6 +36,7 @@ public final class SignalStore {
|
|||
private final PaymentsValues paymentsValues;
|
||||
private final ProxyValues proxyValues;
|
||||
private final RateLimitValues rateLimitValues;
|
||||
private final ChatColorsValues chatColorsValues;
|
||||
|
||||
private SignalStore() {
|
||||
this.store = new KeyValueStore(ApplicationDependencies.getApplication());
|
||||
|
@ -57,6 +58,7 @@ public final class SignalStore {
|
|||
this.paymentsValues = new PaymentsValues(store);
|
||||
this.proxyValues = new ProxyValues(store);
|
||||
this.rateLimitValues = new RateLimitValues(store);
|
||||
this.chatColorsValues = new ChatColorsValues(store);
|
||||
}
|
||||
|
||||
public static void onFirstEverAppLaunch() {
|
||||
|
@ -78,6 +80,7 @@ public final class SignalStore {
|
|||
paymentsValues().onFirstEverAppLaunch();
|
||||
proxy().onFirstEverAppLaunch();
|
||||
rateLimit().onFirstEverAppLaunch();
|
||||
chatColorsValues().onFirstEverAppLaunch();
|
||||
}
|
||||
|
||||
public static List<String> getKeysToIncludeInBackup() {
|
||||
|
@ -100,6 +103,7 @@ public final class SignalStore {
|
|||
keys.addAll(paymentsValues().getKeysToIncludeInBackup());
|
||||
keys.addAll(proxy().getKeysToIncludeInBackup());
|
||||
keys.addAll(rateLimit().getKeysToIncludeInBackup());
|
||||
keys.addAll(chatColorsValues().getKeysToIncludeInBackup());
|
||||
return keys;
|
||||
}
|
||||
|
||||
|
@ -184,6 +188,10 @@ public final class SignalStore {
|
|||
return INSTANCE.rateLimitValues;
|
||||
}
|
||||
|
||||
public static @NonNull ChatColorsValues chatColorsValues() {
|
||||
return INSTANCE.chatColorsValues;
|
||||
}
|
||||
|
||||
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
|
||||
return new GroupsV2AuthorizationSignalStoreCache(getStore());
|
||||
}
|
||||
|
|
|
@ -69,6 +69,6 @@ abstract class SignalStoreValues {
|
|||
}
|
||||
|
||||
void remove(@NonNull String key) {
|
||||
store.beginWrite().remove(key);
|
||||
store.beginWrite().remove(key).apply();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.longmessage;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.SpannableString;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
|
@ -23,21 +22,21 @@ import com.annimon.stream.Stream;
|
|||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.ConversationItemFooter;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ColorizerView;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.WindowUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.thoughtcrime.securesms.util.ThemeUtil.isDarkTheme;
|
||||
|
||||
public class LongMessageActivity extends PassphraseRequiredActivity {
|
||||
|
@ -51,8 +50,10 @@ public class LongMessageActivity extends PassphraseRequiredActivity {
|
|||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
|
||||
private Stub<ViewGroup> sentBubble;
|
||||
private Stub<ViewGroup> receivedBubble;
|
||||
private Stub<ViewGroup> sentBubble;
|
||||
private Stub<ViewGroup> receivedBubble;
|
||||
private ColorizerView colorizerView;
|
||||
private BubbleLayoutListener bubbleLayoutListener;
|
||||
|
||||
private LongMessageViewModel viewModel;
|
||||
|
||||
|
@ -78,6 +79,9 @@ public class LongMessageActivity extends PassphraseRequiredActivity {
|
|||
|
||||
sentBubble = new Stub<>(findViewById(R.id.longmessage_sent_stub));
|
||||
receivedBubble = new Stub<>(findViewById(R.id.longmessage_received_stub));
|
||||
colorizerView = findViewById(R.id.colorizer);
|
||||
|
||||
bubbleLayoutListener = new BubbleLayoutListener();
|
||||
|
||||
initViewModel(getIntent().getLongExtra(KEY_MESSAGE_ID, -1), getIntent().getBooleanExtra(KEY_IS_MMS, false));
|
||||
|
||||
|
@ -130,10 +134,14 @@ public class LongMessageActivity extends PassphraseRequiredActivity {
|
|||
|
||||
if (message.get().getMessageRecord().isOutgoing()) {
|
||||
bubble = sentBubble.get();
|
||||
bubble.getBackground().setColorFilter(ContextCompat.getColor(this, R.color.signal_background_secondary), PorterDuff.Mode.MULTIPLY);
|
||||
colorizerView.setVisibility(View.VISIBLE);
|
||||
colorizerView.setBackground(message.get().getMessageRecord().getRecipient().getChatColors().getChatBubbleMask());
|
||||
bubble.getBackground().setColorFilter(message.get().getMessageRecord().getRecipient().getChatColors().getChatBubbleColorFilter());
|
||||
bubble.addOnLayoutChangeListener(bubbleLayoutListener);
|
||||
bubbleLayoutListener.onLayoutChange(bubble, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
} else {
|
||||
bubble = receivedBubble.get();
|
||||
bubble.getBackground().setColorFilter(message.get().getMessageRecord().getRecipient().getColor().toConversationColor(this), PorterDuff.Mode.MULTIPLY);
|
||||
bubble.getBackground().setColorFilter(ContextCompat.getColor(this, R.color.signal_background_secondary), PorterDuff.Mode.MULTIPLY);
|
||||
}
|
||||
|
||||
EmojiTextView text = bubble.findViewById(R.id.longmessage_text);
|
||||
|
@ -146,7 +154,7 @@ public class LongMessageActivity extends PassphraseRequiredActivity {
|
|||
text.setText(styledBody);
|
||||
text.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
text.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().getMessageFontSize());
|
||||
if (message.get().getMessageRecord().isOutgoing()) {
|
||||
if (!message.get().getMessageRecord().isOutgoing()) {
|
||||
text.setMentionBackgroundTint(ContextCompat.getColor(this, isDarkTheme(this) ? R.color.core_grey_60 : R.color.core_grey_20));
|
||||
} else {
|
||||
text.setMentionBackgroundTint(ContextCompat.getColor(this, R.color.transparent_black_40));
|
||||
|
@ -171,4 +179,13 @@ public class LongMessageActivity extends PassphraseRequiredActivity {
|
|||
}
|
||||
return messageBody;
|
||||
}
|
||||
|
||||
private final class BubbleLayoutListener implements View.OnLayoutChangeListener {
|
||||
@Override
|
||||
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
|
||||
Projection projection = Projection.relativeToViewWithCommonRoot(v, colorizerView, new Projection.Corners(16));
|
||||
|
||||
colorizerView.setProjections(Collections.singletonList(projection));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ColorizerView;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController;
|
||||
|
@ -42,6 +44,7 @@ public final class MessageDetailsActivity extends PassphraseRequiredActivity {
|
|||
private GlideRequests glideRequests;
|
||||
private MessageDetailsViewModel viewModel;
|
||||
private MessageDetailsAdapter adapter;
|
||||
private Colorizer colorizer;
|
||||
|
||||
private DynamicTheme dynamicTheme = new DynamicTheme();
|
||||
|
||||
|
@ -95,11 +98,15 @@ public final class MessageDetailsActivity extends PassphraseRequiredActivity {
|
|||
}
|
||||
|
||||
private void initializeList() {
|
||||
RecyclerView list = findViewById(R.id.message_details_list);
|
||||
adapter = new MessageDetailsAdapter(this, glideRequests);
|
||||
RecyclerView list = findViewById(R.id.message_details_list);
|
||||
ColorizerView colorizerView = findViewById(R.id.message_details_colorizer);
|
||||
|
||||
colorizer = new Colorizer(colorizerView);
|
||||
adapter = new MessageDetailsAdapter(this, glideRequests, colorizer);
|
||||
|
||||
list.setAdapter(adapter);
|
||||
list.setItemAnimator(null);
|
||||
colorizer.attachToRecyclerView(list);
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
|
@ -116,6 +123,7 @@ public final class MessageDetailsActivity extends PassphraseRequiredActivity {
|
|||
adapter.submitList(convertToRows(details));
|
||||
}
|
||||
});
|
||||
viewModel.getRecipient().observe(this, recipient -> colorizer.onChatColorsChanged(recipient.getChatColors()));
|
||||
}
|
||||
|
||||
private void initializeVideoPlayer() {
|
||||
|
|
|
@ -12,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -22,12 +23,14 @@ final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.Mess
|
|||
|
||||
private final LifecycleOwner lifecycleOwner;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Colorizer colorizer;
|
||||
private boolean running;
|
||||
|
||||
MessageDetailsAdapter(@NonNull LifecycleOwner lifecycleOwner, @NonNull GlideRequests glideRequests) {
|
||||
MessageDetailsAdapter(@NonNull LifecycleOwner lifecycleOwner, @NonNull GlideRequests glideRequests, @NonNull Colorizer colorizer) {
|
||||
super(new MessageDetailsDiffer());
|
||||
this.lifecycleOwner = lifecycleOwner;
|
||||
this.glideRequests = glideRequests;
|
||||
this.colorizer = colorizer;
|
||||
this.running = true;
|
||||
}
|
||||
|
||||
|
@ -35,7 +38,7 @@ final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.Mess
|
|||
public @NonNull RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
case MessageDetailsViewState.MESSAGE_HEADER:
|
||||
return new MessageHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_header, parent, false), glideRequests);
|
||||
return new MessageHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_header, parent, false), glideRequests, colorizer);
|
||||
case MessageDetailsViewState.RECIPIENT_HEADER:
|
||||
return new RecipientHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_recipient_header, parent, false));
|
||||
case MessageDetailsViewState.RECIPIENT:
|
||||
|
|
|
@ -27,14 +27,14 @@ final class MessageDetailsViewModel extends ViewModel {
|
|||
messageDetails = Transformations.switchMap(messageRecord, repository::getMessageDetails);
|
||||
}
|
||||
|
||||
@NonNull LiveData<MaterialColor> getRecipientColor() {
|
||||
return Transformations.distinctUntilChanged(Transformations.map(recipient, Recipient::getColor));
|
||||
}
|
||||
|
||||
@NonNull LiveData<MessageDetails> getMessageDetails() {
|
||||
return messageDetails;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Recipient> getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
static final class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final RecipientId recipientId;
|
||||
|
|
|
@ -3,9 +3,6 @@ package org.thoughtcrime.securesms.messagedetails;
|
|||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewStub;
|
||||
|
@ -16,19 +13,22 @@ import androidx.annotation.Nullable;
|
|||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.ClipProjectionDrawable;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.MaskDrawable;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
|
@ -39,30 +39,34 @@ import org.whispersystems.libsignal.util.guava.Optional;
|
|||
import java.sql.Date;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable {
|
||||
private final TextView sentDate;
|
||||
private final TextView receivedDate;
|
||||
private final TextView expiresIn;
|
||||
private final TextView transport;
|
||||
private final View expiresGroup;
|
||||
private final View receivedGroup;
|
||||
private final TextView errorText;
|
||||
private final View resendButton;
|
||||
private final View messageMetadata;
|
||||
private final ViewStub updateStub;
|
||||
private final ViewStub sentStub;
|
||||
private final ViewStub receivedStub;
|
||||
private final MaskDrawable maskDrawable;
|
||||
final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable, Colorizable {
|
||||
private final TextView sentDate;
|
||||
private final TextView receivedDate;
|
||||
private final TextView expiresIn;
|
||||
private final TextView transport;
|
||||
private final View expiresGroup;
|
||||
private final View receivedGroup;
|
||||
private final TextView errorText;
|
||||
private final View resendButton;
|
||||
private final View messageMetadata;
|
||||
private final ViewStub updateStub;
|
||||
private final ViewStub sentStub;
|
||||
private final ViewStub receivedStub;
|
||||
private final ClipProjectionDrawable clipProjectionDrawable;
|
||||
private final Colorizer colorizer;
|
||||
|
||||
private GlideRequests glideRequests;
|
||||
private ConversationItem conversationItem;
|
||||
private ExpiresUpdater expiresUpdater;
|
||||
|
||||
MessageHeaderViewHolder(@NonNull View itemView, GlideRequests glideRequests) {
|
||||
MessageHeaderViewHolder(@NonNull View itemView, GlideRequests glideRequests, @NonNull Colorizer colorizer) {
|
||||
super(itemView);
|
||||
this.glideRequests = glideRequests;
|
||||
this.colorizer = colorizer;
|
||||
|
||||
sentDate = itemView.findViewById(R.id.message_details_header_sent_time);
|
||||
receivedDate = itemView.findViewById(R.id.message_details_header_received_time);
|
||||
|
@ -77,8 +81,8 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements G
|
|||
sentStub = itemView.findViewById(R.id.message_details_header_message_view_sent_multimedia);
|
||||
receivedStub = itemView.findViewById(R.id.message_details_header_message_view_received_multimedia);
|
||||
|
||||
maskDrawable = new MaskDrawable(itemView.getBackground());
|
||||
itemView.setBackground(maskDrawable);
|
||||
clipProjectionDrawable = new ClipProjectionDrawable(itemView.getBackground());
|
||||
itemView.setBackground(clipProjectionDrawable);
|
||||
}
|
||||
|
||||
void bind(@NonNull LifecycleOwner lifecycleOwner, @Nullable ConversationMessage conversationMessage, boolean running) {
|
||||
|
@ -117,7 +121,8 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements G
|
|||
false,
|
||||
false,
|
||||
new AttachmentMediaSourceFactory(conversationItem.getContext()),
|
||||
true);
|
||||
true,
|
||||
colorizer);
|
||||
}
|
||||
|
||||
private void bindErrorState(MessageRecord messageRecord) {
|
||||
|
@ -213,14 +218,13 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements G
|
|||
@Override
|
||||
public void showProjectionArea() {
|
||||
conversationItem.showProjectionArea();
|
||||
maskDrawable.setMask(null);
|
||||
updateProjections();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideProjectionArea() {
|
||||
conversationItem.hideProjectionArea();
|
||||
maskDrawable.setMask(conversationItem.getThumbnailMaskingRect((ViewGroup) itemView));
|
||||
maskDrawable.setCorners(conversationItem.getThumbnailCornerMask(itemView).getRadii());
|
||||
updateProjections();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -234,7 +238,7 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements G
|
|||
}
|
||||
|
||||
@Override
|
||||
public @NonNull GiphyMp4Projection getProjection(@NonNull RecyclerView recyclerview) {
|
||||
public @NonNull Projection getProjection(@NonNull ViewGroup recyclerview) {
|
||||
return conversationItem.getProjection(recyclerview);
|
||||
}
|
||||
|
||||
|
@ -243,6 +247,26 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder implements G
|
|||
return conversationItem.canPlayContent();
|
||||
}
|
||||
|
||||
@NotNull @Override public List<Projection> getColorizerProjections() {
|
||||
List<Projection> projections = conversationItem.getColorizerProjections();
|
||||
updateProjections();
|
||||
return projections;
|
||||
}
|
||||
|
||||
private void updateProjections() {
|
||||
Set<Projection> projections = new HashSet<>();
|
||||
|
||||
if (canPlayContent()) {
|
||||
projections.add(conversationItem.getProjection((ViewGroup) itemView));
|
||||
}
|
||||
|
||||
projections.addAll(Stream.of(conversationItem.getColorizerProjections())
|
||||
.map(p -> Projection.translateFromRootToDescendantCoords(p, itemView))
|
||||
.toList());
|
||||
|
||||
clipProjectionDrawable.setProjections(projections);
|
||||
}
|
||||
|
||||
private class ExpiresUpdater implements Runnable {
|
||||
|
||||
private final long expireStartedTimestamp;
|
||||
|
|
|
@ -10,6 +10,8 @@ import org.signal.core.util.logging.Log;
|
|||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
|
@ -231,17 +233,8 @@ public class LegacyMigrationJob extends MigrationJob {
|
|||
|
||||
if (lastSeenVersion < COLOR_MIGRATION) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
DatabaseFactory.getRecipientDatabase(context).updateSystemContactColors((name, color) -> {
|
||||
if (color != null) {
|
||||
try {
|
||||
return MaterialColor.fromSerialized(color);
|
||||
} catch (MaterialColor.UnknownColorException e) {
|
||||
Log.w(TAG, "Encountered an unknown color during legacy color migration.", e);
|
||||
return ContactColorsLegacy.generateFor(name);
|
||||
}
|
||||
}
|
||||
return ContactColorsLegacy.generateFor(name);
|
||||
});
|
||||
//noinspection deprecation
|
||||
DatabaseFactory.getRecipientDatabase(context).updateSystemContactColors();
|
||||
Log.i(TAG, "Color migration took " + (System.currentTimeMillis() - startTime) + " ms");
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
|||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
|
@ -97,7 +98,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||
|
||||
} else {
|
||||
setContentTitle(context.getString(R.string.SingleRecipientNotificationBuilder_signal));
|
||||
setLargeIcon(new GeneratedContactPhoto("Unknown", R.drawable.ic_profile_outline_40).asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context)));
|
||||
setLargeIcon(new GeneratedContactPhoto("Unknown", R.drawable.ic_profile_outline_40).asDrawable(context, ChatColorsPalette.UNKNOWN_CONTACT));
|
||||
}
|
||||
|
||||
setShortcutId(ConversationUtil.getShortcutId(recipient));
|
||||
|
@ -123,10 +124,10 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
|
|||
context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height))
|
||||
.get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
return fallbackContactPhoto.asDrawable(context, recipient.getColor().toConversationColor(context));
|
||||
return fallbackContactPhoto.asDrawable(context, recipient.getChatColors());
|
||||
}
|
||||
} else {
|
||||
return fallbackContactPhoto.asDrawable(context, recipient.getColor().toConversationColor(context));
|
||||
return fallbackContactPhoto.asDrawable(context, recipient.getChatColors());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ import android.text.SpannableStringBuilder
|
|||
import androidx.core.app.TaskStackBuilder
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.contacts.TurnOffContactJoinedNotificationsActivity
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.notifications.DeleteNotificationReceiver
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver
|
||||
|
@ -52,7 +52,7 @@ data class NotificationConversation(
|
|||
return if (SignalStore.settings().messageNotificationsPrivacy.isDisplayContact) {
|
||||
recipient.getContactDrawable(context)
|
||||
} else {
|
||||
GeneratedContactPhoto("Unknown", R.drawable.ic_profile_outline_40).asDrawable(context, ContactColors.UNKNOWN_COLOR.toConversationColor(context))
|
||||
GeneratedContactPhoto("Unknown", R.drawable.ic_profile_outline_40).asDrawable(context, ChatColorsPalette.UNKNOWN_CONTACT)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -56,12 +56,12 @@ fun Recipient.getContactDrawable(context: Context): Drawable? {
|
|||
)
|
||||
.get()
|
||||
} catch (e: InterruptedException) {
|
||||
fallbackContactPhoto.asDrawable(context, color.toConversationColor(context))
|
||||
fallbackContactPhoto.asDrawable(context, chatColors)
|
||||
} catch (e: ExecutionException) {
|
||||
fallbackContactPhoto.asDrawable(context, color.toConversationColor(context))
|
||||
fallbackContactPhoto.asDrawable(context, chatColors)
|
||||
}
|
||||
} else {
|
||||
fallbackContactPhoto.asDrawable(context, color.toConversationColor(context))
|
||||
fallbackContactPhoto.asDrawable(context, chatColors)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -90,7 +90,6 @@ public class PaymentsHomeFragment extends LoggingFragment {
|
|||
viewModel = ViewModelProviders.of(this, new PaymentsHomeViewModel.Factory()).get(PaymentsHomeViewModel.class);
|
||||
|
||||
viewModel.getList().observe(getViewLifecycleOwner(), list -> {
|
||||
// TODO [alex] -- this is a bit of a hack
|
||||
boolean hadPaymentItems = Stream.of(adapter.getCurrentList()).anyMatch(model -> model instanceof PaymentItem);
|
||||
|
||||
if (!hadPaymentItems) {
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.signal.core.util.logging.Log;
|
|||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
|
||||
|
@ -111,7 +112,7 @@ public class EditProfileFragment extends LoggingFragment {
|
|||
|
||||
if (data != null && data.getBooleanExtra("delete", false)) {
|
||||
viewModel.setAvatar(null);
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_solid_white_24).asDrawable(requireActivity(), getResources().getColor(R.color.grey_400)));
|
||||
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_camera_solid_white_24).asDrawable(requireActivity(), ChatColorsPalette.UNKNOWN_CONTACT));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.components.AvatarImageView;
|
|||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto20dp;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
|
@ -122,8 +123,8 @@ public class ReviewBannerView extends LinearLayout {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Drawable newFallbackDrawable(@NonNull Context context, int color, boolean inverted) {
|
||||
return new FallbackPhoto20dp(getFallbackResId()).asDrawable(context, color, inverted);
|
||||
protected Drawable newFallbackDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
return new FallbackPhoto20dp(getFallbackResId()).asDrawable(context, chatColors, inverted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,12 +12,9 @@ import androidx.annotation.WorkerThread;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
|
@ -26,6 +23,8 @@ import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
|||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.TransparentContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting;
|
||||
|
@ -84,7 +83,6 @@ public class Recipient {
|
|||
private final VibrateState callVibrate;
|
||||
private final Uri messageRingtone;
|
||||
private final Uri callRingtone;
|
||||
private final MaterialColor color;
|
||||
private final Optional<Integer> defaultSubscriptionId;
|
||||
private final int expireMessages;
|
||||
private final RegisteredState registered;
|
||||
|
@ -108,6 +106,7 @@ public class Recipient {
|
|||
private final byte[] storageId;
|
||||
private final MentionSetting mentionSetting;
|
||||
private final ChatWallpaper wallpaper;
|
||||
private final ChatColors chatColors;
|
||||
private final String about;
|
||||
private final String aboutEmoji;
|
||||
private final ProfileName systemProfileName;
|
||||
|
@ -330,7 +329,6 @@ public class Recipient {
|
|||
this.callVibrate = VibrateState.DEFAULT;
|
||||
this.messageRingtone = null;
|
||||
this.callRingtone = null;
|
||||
this.color = null;
|
||||
this.insightsBannerTier = InsightsBannerTier.TIER_TWO;
|
||||
this.defaultSubscriptionId = Optional.absent();
|
||||
this.expireMessages = 0;
|
||||
|
@ -354,6 +352,7 @@ public class Recipient {
|
|||
this.storageId = null;
|
||||
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
|
||||
this.wallpaper = null;
|
||||
this.chatColors = null;
|
||||
this.about = null;
|
||||
this.aboutEmoji = null;
|
||||
this.systemProfileName = ProfileName.EMPTY;
|
||||
|
@ -379,7 +378,6 @@ public class Recipient {
|
|||
this.callVibrate = details.callVibrateState;
|
||||
this.messageRingtone = details.messageRingtone;
|
||||
this.callRingtone = details.callRingtone;
|
||||
this.color = details.color;
|
||||
this.insightsBannerTier = details.insightsBannerTier;
|
||||
this.defaultSubscriptionId = details.defaultSubscriptionId;
|
||||
this.expireMessages = details.expireMessages;
|
||||
|
@ -403,6 +401,7 @@ public class Recipient {
|
|||
this.storageId = details.storageId;
|
||||
this.mentionSetting = details.mentionSetting;
|
||||
this.wallpaper = details.wallpaper;
|
||||
this.chatColors = details.chatColors;
|
||||
this.about = details.about;
|
||||
this.aboutEmoji = details.aboutEmoji;
|
||||
this.systemProfileName = details.systemProfileName;
|
||||
|
@ -556,24 +555,6 @@ public class Recipient {
|
|||
return StringUtil.isolateBidi(name);
|
||||
}
|
||||
|
||||
public @NonNull MaterialColor getColor() {
|
||||
if (isGroupInternal()) {
|
||||
return MaterialColor.GROUP;
|
||||
} else if (color != null) {
|
||||
return color;
|
||||
} else if (groupName != null || profileSharing) {
|
||||
Log.w(TAG, "Had no color for " + id + "! Saving a new one.");
|
||||
|
||||
Context context = ApplicationDependencies.getApplication();
|
||||
MaterialColor color = ContactColors.generateFor(getDisplayName(context));
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getRecipientDatabase(context).setColorIfNotSet(id, color));
|
||||
return color;
|
||||
} else {
|
||||
return ContactColors.UNKNOWN_COLOR;
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull Optional<UUID> getUuid() {
|
||||
return Optional.fromNullable(uuid);
|
||||
}
|
||||
|
@ -775,11 +756,11 @@ public class Recipient {
|
|||
}
|
||||
|
||||
public @NonNull Drawable getFallbackContactPhotoDrawable(Context context, boolean inverted, @Nullable FallbackPhotoProvider fallbackPhotoProvider) {
|
||||
return getFallbackContactPhoto(Util.firstNonNull(fallbackPhotoProvider, DEFAULT_FALLBACK_PHOTO_PROVIDER)).asDrawable(context, getColor().toAvatarColor(context), inverted);
|
||||
return getFallbackContactPhoto(Util.firstNonNull(fallbackPhotoProvider, DEFAULT_FALLBACK_PHOTO_PROVIDER)).asDrawable(context, getChatColors(), inverted);
|
||||
}
|
||||
|
||||
public @NonNull Drawable getSmallFallbackContactPhotoDrawable(Context context, boolean inverted, @Nullable FallbackPhotoProvider fallbackPhotoProvider) {
|
||||
return getFallbackContactPhoto(Util.firstNonNull(fallbackPhotoProvider, DEFAULT_FALLBACK_PHOTO_PROVIDER)).asSmallDrawable(context, getColor().toAvatarColor(context), inverted);
|
||||
return getFallbackContactPhoto(Util.firstNonNull(fallbackPhotoProvider, DEFAULT_FALLBACK_PHOTO_PROVIDER)).asSmallDrawable(context, getChatColors(), inverted);
|
||||
}
|
||||
|
||||
public @NonNull FallbackContactPhoto getFallbackContactPhoto() {
|
||||
|
@ -920,6 +901,32 @@ public class Recipient {
|
|||
return wallpaper != null || SignalStore.wallpaper().hasWallpaperSet();
|
||||
}
|
||||
|
||||
public boolean hasOwnChatColors() {
|
||||
return chatColors != null;
|
||||
}
|
||||
|
||||
public @NonNull ChatColors getChatColors() {
|
||||
if (chatColors != null && chatColors.getId() instanceof ChatColors.Id.Auto) {
|
||||
return getAutoChatColor();
|
||||
} else if (chatColors != null) {
|
||||
return chatColors;
|
||||
} else if (SignalStore.chatColorsValues().hasChatColors()) {
|
||||
return Objects.requireNonNull(SignalStore.chatColorsValues().getChatColors());
|
||||
} else {
|
||||
return getAutoChatColor();
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull ChatColors getAutoChatColor() {
|
||||
if (wallpaper != null) {
|
||||
return wallpaper.getAutoChatColors();
|
||||
} else if (getWallpaper() != null) {
|
||||
return getWallpaper().getAutoChatColors();
|
||||
} else {
|
||||
return ChatColorsPalette.Bubbles.getDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSystemContact() {
|
||||
return contactUri != null;
|
||||
}
|
||||
|
@ -1002,7 +1009,6 @@ public class Recipient {
|
|||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
|
||||
public enum Capability {
|
||||
UNKNOWN(0),
|
||||
SUPPORTED(1),
|
||||
|
@ -1088,7 +1094,6 @@ public class Recipient {
|
|||
callVibrate == other.callVibrate &&
|
||||
Objects.equals(messageRingtone, other.messageRingtone) &&
|
||||
Objects.equals(callRingtone, other.callRingtone) &&
|
||||
color == other.color &&
|
||||
Objects.equals(defaultSubscriptionId, other.defaultSubscriptionId) &&
|
||||
registered == other.registered &&
|
||||
Arrays.equals(profileKey, other.profileKey) &&
|
||||
|
@ -1108,6 +1113,7 @@ public class Recipient {
|
|||
Arrays.equals(storageId, other.storageId) &&
|
||||
mentionSetting == other.mentionSetting &&
|
||||
Objects.equals(wallpaper, other.wallpaper) &&
|
||||
Objects.equals(chatColors, other.chatColors) &&
|
||||
Objects.equals(about, other.about) &&
|
||||
Objects.equals(aboutEmoji, other.aboutEmoji) &&
|
||||
Objects.equals(extras, other.extras);
|
||||
|
|
|
@ -7,7 +7,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
|
@ -38,7 +38,6 @@ public class RecipientDetails {
|
|||
final Uri systemContactPhoto;
|
||||
final Uri contactUri;
|
||||
final Optional<Long> groupAvatarId;
|
||||
final MaterialColor color;
|
||||
final Uri messageRingtone;
|
||||
final Uri callRingtone;
|
||||
final long mutedUntil;
|
||||
|
@ -67,6 +66,7 @@ public class RecipientDetails {
|
|||
final byte[] storageId;
|
||||
final MentionSetting mentionSetting;
|
||||
final ChatWallpaper wallpaper;
|
||||
final ChatColors chatColors;
|
||||
final String about;
|
||||
final String aboutEmoji;
|
||||
final ProfileName systemProfileName;
|
||||
|
@ -91,7 +91,6 @@ public class RecipientDetails {
|
|||
this.e164 = settings.getE164();
|
||||
this.email = settings.getEmail();
|
||||
this.groupId = settings.getGroupId();
|
||||
this.color = settings.getColor();
|
||||
this.messageRingtone = settings.getMessageRingtone();
|
||||
this.callRingtone = settings.getCallRingtone();
|
||||
this.mutedUntil = settings.getMuteUntil();
|
||||
|
@ -120,6 +119,7 @@ public class RecipientDetails {
|
|||
this.storageId = settings.getStorageId();
|
||||
this.mentionSetting = settings.getMentionSetting();
|
||||
this.wallpaper = settings.getWallpaper();
|
||||
this.chatColors = settings.getChatColors();
|
||||
this.about = settings.getAbout();
|
||||
this.aboutEmoji = settings.getAboutEmoji();
|
||||
this.systemProfileName = settings.getSystemProfileName();
|
||||
|
@ -142,7 +142,6 @@ public class RecipientDetails {
|
|||
this.e164 = null;
|
||||
this.email = null;
|
||||
this.groupId = null;
|
||||
this.color = null;
|
||||
this.messageRingtone = null;
|
||||
this.callRingtone = null;
|
||||
this.mutedUntil = 0;
|
||||
|
@ -172,6 +171,7 @@ public class RecipientDetails {
|
|||
this.storageId = null;
|
||||
this.mentionSetting = MentionSetting.ALWAYS_NOTIFY;
|
||||
this.wallpaper = null;
|
||||
this.chatColors = null;
|
||||
this.about = null;
|
||||
this.aboutEmoji = null;
|
||||
this.systemProfileName = ProfileName.EMPTY;
|
||||
|
|
|
@ -140,7 +140,7 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
avatar.setFallbackPhotoProvider(new Recipient.FallbackPhotoProvider() {
|
||||
@Override
|
||||
public @NonNull FallbackContactPhoto getPhotoForLocalNumber() {
|
||||
return new FallbackPhoto80dp(R.drawable.ic_note_80, recipient.getColor().toAvatarColor(requireContext()));
|
||||
return new FallbackPhoto80dp(R.drawable.ic_note_80, recipient.getChatColors());
|
||||
}
|
||||
});
|
||||
avatar.setAvatar(recipient);
|
||||
|
|
|
@ -14,31 +14,25 @@ import android.widget.ImageView;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.takisoft.colorpicker.ColorPickerDialog;
|
||||
import com.takisoft.colorpicker.ColorStateDrawable;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.AvatarPreviewActivity;
|
||||
import org.thoughtcrime.securesms.LoggingFragment;
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.MuteDialog;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.color.MaterialColors;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
|
@ -89,8 +83,6 @@ public class ManageRecipientFragment extends LoggingFragment {
|
|||
private View disappearingMessagesCard;
|
||||
private View disappearingMessagesRow;
|
||||
private TextView disappearingMessages;
|
||||
private View colorRow;
|
||||
private ImageView colorChip;
|
||||
private View blockUnblockCard;
|
||||
private TextView block;
|
||||
private TextView unblock;
|
||||
|
@ -109,7 +101,7 @@ public class ManageRecipientFragment extends LoggingFragment {
|
|||
private View secureCallButton;
|
||||
private View insecureCallButton;
|
||||
private View secureVideoCallButton;
|
||||
private View chatWallpaperButton;
|
||||
private TextView chatWallpaperButton;
|
||||
|
||||
static ManageRecipientFragment newInstance(@NonNull RecipientId recipientId, boolean fromConversation) {
|
||||
ManageRecipientFragment fragment = new ManageRecipientFragment();
|
||||
|
@ -148,8 +140,6 @@ public class ManageRecipientFragment extends LoggingFragment {
|
|||
disappearingMessagesCard = view.findViewById(R.id.recipient_disappearing_messages_card);
|
||||
disappearingMessagesRow = view.findViewById(R.id.disappearing_messages_row);
|
||||
disappearingMessages = view.findViewById(R.id.disappearing_messages);
|
||||
colorRow = view.findViewById(R.id.color_row);
|
||||
colorChip = view.findViewById(R.id.color_chip);
|
||||
blockUnblockCard = view.findViewById(R.id.recipient_block_and_leave_card);
|
||||
block = view.findViewById(R.id.block);
|
||||
unblock = view.findViewById(R.id.unblock);
|
||||
|
@ -294,6 +284,10 @@ public class ManageRecipientFragment extends LoggingFragment {
|
|||
}
|
||||
|
||||
private void presentRecipient(@NonNull Recipient recipient) {
|
||||
Drawable colorCircle = recipient.getChatColors().asCircle();
|
||||
colorCircle.setBounds(0, 0, ViewUtil.dpToPx(16), ViewUtil.dpToPx(16));
|
||||
TextViewCompat.setCompoundDrawablesRelative(chatWallpaperButton, null, null, colorCircle, null);
|
||||
|
||||
if (recipient.isSystemContact()) {
|
||||
contactText.setText(R.string.ManageRecipientActivity_this_person_is_in_your_contacts);
|
||||
contactIcon.setVisibility(View.VISIBLE);
|
||||
|
@ -315,16 +309,16 @@ public class ManageRecipientFragment extends LoggingFragment {
|
|||
disappearingMessagesCard.setVisibility(recipient.isRegistered() ? View.VISIBLE : View.GONE);
|
||||
addToAGroup.setVisibility(recipient.isRegistered() ? View.VISIBLE : View.GONE);
|
||||
|
||||
MaterialColor recipientColor = recipient.getColor();
|
||||
ChatColors recipientColor = recipient.getChatColors();
|
||||
avatar.setFallbackPhotoProvider(new Recipient.FallbackPhotoProvider() {
|
||||
@Override
|
||||
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
|
||||
return new FallbackPhoto80dp(R.drawable.ic_profile_80, recipientColor.toAvatarColor(requireContext()));
|
||||
return new FallbackPhoto80dp(R.drawable.ic_profile_80, recipientColor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull FallbackContactPhoto getPhotoForLocalNumber() {
|
||||
return new FallbackPhoto80dp(R.drawable.ic_note_80, recipientColor.toAvatarColor(requireContext()));
|
||||
return new FallbackPhoto80dp(R.drawable.ic_note_80, recipientColor);
|
||||
}
|
||||
});
|
||||
avatar.setAvatar(recipient);
|
||||
|
@ -334,11 +328,6 @@ public class ManageRecipientFragment extends LoggingFragment {
|
|||
AvatarPreviewActivity.createTransitionBundle(activity, avatar));
|
||||
});
|
||||
|
||||
@ColorInt int color = recipientColor.toActionBarColor(requireContext());
|
||||
Drawable[] colorDrawable = new Drawable[]{ContextCompat.getDrawable(requireContext(), R.drawable.colorpickerpreference_pref_swatch)};
|
||||
colorChip.setImageDrawable(new ColorStateDrawable(colorDrawable, color));
|
||||
colorRow.setOnClickListener(v -> handleColorSelection(color));
|
||||
|
||||
secureCallButton.setVisibility(recipient.isRegistered() && !recipient.isSelf() ? View.VISIBLE : View.GONE);
|
||||
insecureCallButton.setVisibility(!recipient.isRegistered() && !recipient.isSelf() ? View.VISIBLE : View.GONE);
|
||||
secureVideoCallButton.setVisibility(recipient.isRegistered() && !recipient.isSelf() ? View.VISIBLE : View.GONE);
|
||||
|
@ -385,22 +374,6 @@ public class ManageRecipientFragment extends LoggingFragment {
|
|||
}
|
||||
}
|
||||
|
||||
private void handleColorSelection(@ColorInt int currentColor) {
|
||||
@ColorInt int[] colors = MaterialColors.CONVERSATION_PALETTE.asConversationColorArray(requireContext());
|
||||
|
||||
ColorPickerDialog.Params params = new ColorPickerDialog.Params.Builder(requireContext())
|
||||
.setSelectedColor(currentColor)
|
||||
.setColors(colors)
|
||||
.setSize(ColorPickerDialog.SIZE_SMALL)
|
||||
.setSortColors(false)
|
||||
.setColumns(3)
|
||||
.build();
|
||||
|
||||
ColorPickerDialog dialog = new ColorPickerDialog(requireActivity(), color -> viewModel.onSelectColor(color), params);
|
||||
dialog.setTitle(R.string.ManageRecipientActivity_chat_color);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public boolean onMenuItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_edit) {
|
||||
startActivity(EditProfileActivity.getIntentForUserProfileEdit(requireActivity()));
|
||||
|
|
|
@ -82,16 +82,6 @@ final class ManageRecipientRepository {
|
|||
SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until));
|
||||
}
|
||||
|
||||
void setColor(int color) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
MaterialColor selectedColor = MaterialColors.CONVERSATION_PALETTE.getByColor(context, color);
|
||||
if (selectedColor != null) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setColor(recipientId, selectedColor);
|
||||
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(recipientId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void refreshRecipient() {
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
try {
|
||||
|
|
|
@ -240,10 +240,6 @@ public final class ManageRecipientViewModel extends ViewModel {
|
|||
return sharedGroupsCountSummary;
|
||||
}
|
||||
|
||||
void onSelectColor(int color) {
|
||||
manageRecipientRepository.setColor(color);
|
||||
}
|
||||
|
||||
void onGroupClicked(@NonNull Activity activity, @NonNull Recipient recipient) {
|
||||
CommunicationActions.startConversation(activity, recipient, null);
|
||||
activity.finish();
|
||||
|
|
|
@ -26,6 +26,8 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
|
|||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -172,12 +174,8 @@ public final class AvatarUtil {
|
|||
|
||||
private static Drawable getFallback(@NonNull Context context, @NonNull Recipient recipient) {
|
||||
String name = Optional.fromNullable(recipient.getDisplayName(context)).or("");
|
||||
MaterialColor fallbackColor = recipient.getColor();
|
||||
ChatColors fallbackColor = recipient.getChatColors();
|
||||
|
||||
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
|
||||
fallbackColor = ContactColors.generateFor(name);
|
||||
}
|
||||
|
||||
return new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40).asDrawable(context, fallbackColor.toAvatarColor(context));
|
||||
return new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40).asDrawable(context, fallbackColor);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
|||
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.SystemContactPhoto;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
|
@ -177,9 +178,9 @@ public final class ConversationShortcutPhoto implements Key {
|
|||
photoSource = R.drawable.ic_profile_80;
|
||||
}
|
||||
|
||||
FallbackContactPhoto photo = recipient.isSelf() || recipient.isGroup() ? new FallbackPhoto80dp(photoSource, recipient.getColor().toAvatarColor(context))
|
||||
FallbackContactPhoto photo = recipient.isSelf() || recipient.isGroup() ? new FallbackPhoto80dp(photoSource, recipient.getChatColors())
|
||||
: new ShortcutGeneratedContactPhoto(recipient.getDisplayName(context), photoSource, ViewUtil.dpToPx(80), ViewUtil.dpToPx(28));
|
||||
Bitmap toWrap = DrawableUtil.toBitmap(photo.asDrawable(context, recipient.getColor().toAvatarColor(context)), ViewUtil.dpToPx(80), ViewUtil.dpToPx(80));
|
||||
Bitmap toWrap = DrawableUtil.toBitmap(photo.asDrawable(context, recipient.getChatColors()), ViewUtil.dpToPx(80), ViewUtil.dpToPx(80));
|
||||
Bitmap wrapped = DrawableUtil.wrapBitmapForShortcutInfo(toWrap);
|
||||
|
||||
toWrap.recycle();
|
||||
|
@ -198,8 +199,8 @@ public final class ConversationShortcutPhoto implements Key {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Drawable newFallbackDrawable(@NonNull Context context, int color, boolean inverted) {
|
||||
return new FallbackPhoto80dp(getFallbackResId(), color).asDrawable(context, -1);
|
||||
protected Drawable newFallbackDrawable(@NonNull Context context, @NonNull ChatColors chatColors, boolean inverted) {
|
||||
return new FallbackPhoto80dp(getFallbackResId(), chatColors).asDrawable(context, chatColors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue