Update chat colors.

This commit is contained in:
Alex Hart 2021-05-03 11:34:41 -03:00 committed by Greyson Parrelli
parent 36fe150678
commit bcc5d485ab
164 changed files with 5817 additions and 1476 deletions

View file

@ -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'

View file

@ -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();

View file

@ -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";

View file

@ -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));

View file

@ -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);
}

View file

@ -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()) {

View file

@ -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) {

View file

@ -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);

View file

@ -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;
}

View file

@ -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)));
}
}

View file

@ -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() {

View file

@ -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;
}
}

View file

@ -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)
}

View file

@ -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();

View file

@ -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)

View file

@ -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) {

View file

@ -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);
}

View file

@ -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;

View file

@ -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;

View file

@ -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) {

View file

@ -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);

View file

@ -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);
}

View file

@ -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)
}
}
}

View file

@ -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();

View file

@ -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 {

View file

@ -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(), () -> {

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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 {

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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>
}

View file

@ -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
}
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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?)
}

View file

@ -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()
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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())
}
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}
}

View file

@ -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))
}
}
}

View file

@ -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)

View file

@ -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()
}
}

View file

@ -0,0 +1,5 @@
package org.thoughtcrime.securesms.conversation.colors.ui.custom
enum class CustomChatColorEdge {
TOP, BOTTOM
}

View file

@ -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
}
}

View file

@ -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()
}
}
}
}

View file

@ -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()));
}
}

View file

@ -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))
)
}

View file

@ -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,

View file

@ -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);

View file

@ -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;
}

View file

@ -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();

View file

@ -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.

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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();
}

View file

@ -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

View file

@ -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);
});

View file

@ -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);
}
};
}

View file

@ -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,

View file

@ -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) {

View file

@ -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,

View file

@ -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)));

View file

@ -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)
}

View file

@ -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());
}

View file

@ -69,6 +69,6 @@ abstract class SignalStoreValues {
}
void remove(@NonNull String key) {
store.beginWrite().remove(key);
store.beginWrite().remove(key).apply();
}
}

View file

@ -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));
}
}
}

View file

@ -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() {

View file

@ -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:

View file

@ -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;

View file

@ -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;

View file

@ -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");
}

View file

@ -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());
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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) {

View file

@ -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;
}

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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()));

View file

@ -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 {

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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