kopia lustrzana https://github.com/ryukoposting/Signal-Android
Reactions UX polish.
rodzic
0950235ccd
commit
9d3764c5d9
|
@ -46,7 +46,7 @@ public interface BindableConversationItem extends Unbindable {
|
|||
void onAddToContactsClicked(@NonNull Contact contact);
|
||||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(long messageId, boolean isMms);
|
||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||
void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
}
|
||||
|
|
|
@ -79,7 +79,6 @@ public class MaskView extends View {
|
|||
target.getDrawingRect(drawingRect);
|
||||
activityContentView.offsetDescendantRectToMyCoords(target, drawingRect);
|
||||
|
||||
drawingRect.bottom = Math.min(drawingRect.bottom, getBottom() - getPaddingBottom());
|
||||
drawingRect.top += targetParentTranslationY;
|
||||
drawingRect.bottom += targetParentTranslationY;
|
||||
|
||||
|
@ -88,6 +87,7 @@ public class MaskView extends View {
|
|||
|
||||
target.draw(maskCanvas);
|
||||
|
||||
canvas.clipRect(drawingRect.left, Math.max(drawingRect.top, getTop() + getPaddingTop()), drawingRect.right, Math.min(drawingRect.bottom, getBottom() - getPaddingBottom()));
|
||||
canvas.drawBitmap(mask, 0, drawingRect.top, maskPaint);
|
||||
|
||||
mask.recycle();
|
||||
|
|
|
@ -201,6 +201,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
|||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -281,7 +282,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
AttachmentKeyboard.Callback,
|
||||
ConversationReactionOverlay.OnReactionSelectedListener,
|
||||
ReactWithAnyEmojiBottomSheetDialogFragment.Callback,
|
||||
SafetyNumberChangeDialog.Callback
|
||||
SafetyNumberChangeDialog.Callback,
|
||||
ReactionsBottomSheetDialogFragment.Callback
|
||||
{
|
||||
|
||||
private static final int SHORTCUT_ICON_SIZE = Build.VERSION.SDK_INT >= 26 ? ViewUtil.dpToPx(72) : ViewUtil.dpToPx(48 + 16 * 2);
|
||||
|
@ -2763,6 +2765,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
typingTextWatcher.setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReactionsDialogDismissed() {
|
||||
reactionOverlay.hideMask();
|
||||
}
|
||||
|
||||
// Listeners
|
||||
|
||||
private class QuickCameraToggleListener implements OnClickListener {
|
||||
|
@ -2950,6 +2957,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleReactionDetails(@NonNull View maskTarget) {
|
||||
reactionOverlay.showMask(maskTarget, titleView.getMeasuredHeight(), panelParent.getMeasuredHeight());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCursorChanged() {
|
||||
if (!reactionOverlay.isShowing()) {
|
||||
|
|
|
@ -1014,6 +1014,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
void onCursorChanged();
|
||||
void onListVerticalTranslationChanged(float translationY);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void handleReactionDetails(@NonNull View maskTarget);
|
||||
}
|
||||
|
||||
private class ConversationScrollListener extends OnScrollListener {
|
||||
|
@ -1264,9 +1265,10 @@ public class ConversationFragment extends LoggingFragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onReactionClicked(long messageId, boolean isMms) {
|
||||
public void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms) {
|
||||
if (getContext() == null) return;
|
||||
|
||||
listener.handleReactionDetails(reactionTarget);
|
||||
ReactionsBottomSheetDialogFragment.create(messageId, isMms).show(requireFragmentManager(), null);
|
||||
}
|
||||
|
||||
|
|
|
@ -970,7 +970,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
reactionsView.setOnClickListener(v -> {
|
||||
if (eventListener == null) return;
|
||||
|
||||
eventListener.onReactionClicked(current.getId(), current.isMms());
|
||||
eventListener.onReactionClicked(this, current.getId(), current.isMms());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,9 @@ import android.animation.AnimatorSet;
|
|||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
|
@ -19,7 +21,6 @@ import android.widget.RelativeLayout;
|
|||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
|
@ -32,10 +33,11 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.MaskView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
@ -91,6 +93,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
|||
private OnHideListener onHideListener;
|
||||
|
||||
private AnimatorSet revealAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet revealMaskAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideAllButMaskAnimatorSet = new AnimatorSet();
|
||||
private AnimatorSet hideMaskAnimatorSet = new AnimatorSet();
|
||||
|
@ -185,16 +188,31 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
|||
maskView.setTarget(maskTarget);
|
||||
|
||||
hideAnimatorSet.end();
|
||||
toolbar.setVisibility(VISIBLE);
|
||||
setVisibility(View.VISIBLE);
|
||||
revealAnimatorSet.start();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
this.activity = activity;
|
||||
originalStatusBarColor = activity.getWindow().getStatusBarColor();
|
||||
activity.getWindow().setStatusBarColor(ContextCompat.getColor(activity, R.color.action_mode_status_bar));
|
||||
activity.getWindow().setStatusBarColor(ThemeUtil.getThemedColor(getContext(), R.attr.reactions_overlay_toolbar_background_color));
|
||||
|
||||
if (!ThemeUtil.isDarkTheme(getContext()) && Build.VERSION.SDK_INT >= 23) {
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(activity.getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showMask(@NonNull View maskTarget, int maskPaddingTop, int maskPaddingBottom) {
|
||||
maskView.setPadding(0, maskPaddingTop, 0, maskPaddingBottom);
|
||||
maskView.setTarget(maskTarget);
|
||||
|
||||
hideAnimatorSet.end();
|
||||
toolbar.setVisibility(GONE);
|
||||
setVisibility(VISIBLE);
|
||||
revealMaskAnimatorSet.start();
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
maskView.setTarget(null);
|
||||
hideInternal(hideAnimatorSet, onHideListener);
|
||||
|
@ -220,6 +238,11 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
|||
|
||||
if (Build.VERSION.SDK_INT >= 21 && activity != null) {
|
||||
activity.getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
|
||||
if (!ThemeUtil.isDarkTheme(getContext()) && Build.VERSION.SDK_INT >= 23) {
|
||||
activity.getWindow().getDecorView().setSystemUiVisibility(activity.getWindow().getDecorView().getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
|
||||
}
|
||||
|
||||
activity = null;
|
||||
}
|
||||
|
||||
|
@ -358,7 +381,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
|||
view.setTranslationY(0);
|
||||
|
||||
boolean isAtCustomIndex = i == customEmojiIndex;
|
||||
boolean isNotAtCustomIndexAndOldEmojiMatches = !isAtCustomIndex && ReactionEmoji.values()[i].emoji.equals(oldEmoji);
|
||||
boolean isNotAtCustomIndexAndOldEmojiMatches = !isAtCustomIndex && oldEmoji != null && ReactionEmoji.values()[i].emoji.equals(EmojiUtil.getCanonicalRepresentation(oldEmoji));
|
||||
boolean isAtCustomIndexAndOldEmojiExists = isAtCustomIndex && oldEmoji != null;
|
||||
|
||||
if (!foundSelected &&
|
||||
|
@ -379,13 +402,13 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
|||
view.setImageEmoji(oldEmoji);
|
||||
view.setTag(oldEmoji);
|
||||
} else {
|
||||
view.setImageEmoji(ReactionEmoji.values()[i].emoji);
|
||||
view.setImageEmoji(SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[i].emoji));
|
||||
}
|
||||
} else if (isAtCustomIndex) {
|
||||
view.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.ic_any_emoji_32));
|
||||
view.setImageDrawable(ThemeUtil.getThemedDrawable(getContext(), R.attr.reactions_overlay_custom_emoji_icon));
|
||||
view.setTag(null);
|
||||
} else {
|
||||
view.setImageEmoji(ReactionEmoji.values()[i].emoji);
|
||||
view.setImageEmoji(SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[i].emoji));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -447,7 +470,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
|||
if (selected == customEmojiIndex) {
|
||||
onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].getTag() != null);
|
||||
} else {
|
||||
onReactionSelectedListener.onReactionSelected(messageRecord, ReactionEmoji.values()[selected].emoji);
|
||||
onReactionSelectedListener.onReactionSelected(messageRecord, SignalStore.emojiValues().getPreferredVariation(ReactionEmoji.values()[selected].emoji));
|
||||
}
|
||||
} else {
|
||||
hide();
|
||||
|
@ -534,6 +557,9 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
|||
revealAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
revealAnimatorSet.playTogether(reveals);
|
||||
|
||||
revealMaskAnimatorSet.setInterpolator(INTERPOLATOR);
|
||||
revealMaskAnimatorSet.playTogether(overlayRevealAnim);
|
||||
|
||||
List<Animator> hides = Stream.of(emojiViews)
|
||||
.mapIndexed((idx, v) -> {
|
||||
Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide);
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
|
||||
public class EmojiValues extends SignalStoreValues {
|
||||
|
||||
private static final String PREFIX = "emojiPref__";
|
||||
|
||||
EmojiValues(@NonNull KeyValueStore store) {
|
||||
super(store);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onFirstEverAppLaunch() {
|
||||
|
||||
}
|
||||
|
||||
public void setPreferredVariation(@NonNull String emoji) {
|
||||
String canonical = EmojiUtil.getCanonicalRepresentation(emoji);
|
||||
|
||||
if (canonical.equals(emoji)) {
|
||||
getStore().beginWrite().remove(PREFIX + canonical).apply();
|
||||
} else {
|
||||
putString(PREFIX + canonical, emoji);
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull String getPreferredVariation(@NonNull String emoji) {
|
||||
String canonical = EmojiUtil.getCanonicalRepresentation(emoji);
|
||||
|
||||
return getString(PREFIX + canonical, emoji);
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ public final class SignalStore {
|
|||
private final TooltipValues tooltipValues;
|
||||
private final MiscellaneousValues misc;
|
||||
private final InternalValues internalValues;
|
||||
private final EmojiValues emojiValues;
|
||||
|
||||
private SignalStore() {
|
||||
this.store = ApplicationDependencies.getKeyValueStore();
|
||||
|
@ -36,6 +37,7 @@ public final class SignalStore {
|
|||
this.tooltipValues = new TooltipValues(store);
|
||||
this.misc = new MiscellaneousValues(store);
|
||||
this.internalValues = new InternalValues(store);
|
||||
this.emojiValues = new EmojiValues(store);
|
||||
}
|
||||
|
||||
public static void onFirstEverAppLaunch() {
|
||||
|
@ -86,6 +88,10 @@ public final class SignalStore {
|
|||
return INSTANCE.internalValues;
|
||||
}
|
||||
|
||||
public static @NonNull EmojiValues emojiValues() {
|
||||
return INSTANCE.emojiValues;
|
||||
}
|
||||
|
||||
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
|
||||
return new GroupsV2AuthorizationSignalStoreCache(getStore());
|
||||
}
|
||||
|
|
|
@ -2,15 +2,25 @@ package org.thoughtcrime.securesms.reactions;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
final class EmojiCount {
|
||||
private final String baseEmoji;
|
||||
private final String displayEmoji;
|
||||
private final int count;
|
||||
import java.util.List;
|
||||
|
||||
EmojiCount(@NonNull String baseEmoji, @NonNull String emoji, int count) {
|
||||
final class EmojiCount {
|
||||
|
||||
static EmojiCount all(@NonNull List<ReactionDetails> reactions) {
|
||||
return new EmojiCount("", "", reactions);
|
||||
}
|
||||
|
||||
private final String baseEmoji;
|
||||
private final String displayEmoji;
|
||||
private final List<ReactionDetails> reactions;
|
||||
|
||||
EmojiCount(@NonNull String baseEmoji,
|
||||
@NonNull String emoji,
|
||||
@NonNull List<ReactionDetails> reactions)
|
||||
{
|
||||
this.baseEmoji = baseEmoji;
|
||||
this.displayEmoji = emoji;
|
||||
this.count = count;
|
||||
this.reactions = reactions;
|
||||
}
|
||||
|
||||
public @NonNull String getBaseEmoji() {
|
||||
|
@ -22,6 +32,10 @@ final class EmojiCount {
|
|||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
return reactions.size();
|
||||
}
|
||||
|
||||
public @NonNull List<ReactionDetails> getReactions() {
|
||||
return reactions;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
class ReactionDetails {
|
||||
private final Recipient sender;
|
||||
private final String baseEmoji;
|
||||
private final String displayEmoji;
|
||||
private final long timestamp;
|
||||
|
||||
ReactionDetails(@NonNull Recipient sender, @NonNull String baseEmoji, @NonNull String displayEmoji, long timestamp) {
|
||||
this.sender = sender;
|
||||
this.baseEmoji = baseEmoji;
|
||||
this.displayEmoji = displayEmoji;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public @NonNull String getBaseEmoji() {
|
||||
return baseEmoji;
|
||||
}
|
||||
|
||||
public @NonNull String getDisplayEmoji() {
|
||||
return displayEmoji;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class ReactionEmojiCountAdapter extends RecyclerView.Adapter<ReactionEmojiCountAdapter.ViewHolder> {
|
||||
|
||||
private List<EmojiCount> emojiCountList = Collections.emptyList();
|
||||
private int totalCount = 0;
|
||||
private int selectedPosition = -1;
|
||||
|
||||
private final OnEmojiCountSelectedListener onEmojiCountSelectedListener;
|
||||
|
||||
ReactionEmojiCountAdapter(@NonNull OnEmojiCountSelectedListener onEmojiCountSelectedListener) {
|
||||
this.onEmojiCountSelectedListener = onEmojiCountSelectedListener;
|
||||
}
|
||||
|
||||
void updateData(@NonNull List<EmojiCount> newEmojiCount) {
|
||||
if (selectedPosition != -1 && selectedPosition != 0) {
|
||||
int emojiPosition = selectedPosition - 1;
|
||||
EmojiCount oldSelection = emojiCountList.get(emojiPosition);
|
||||
int newPosition = -1;
|
||||
|
||||
for (int i = 0; i < newEmojiCount.size(); i++) {
|
||||
if (newEmojiCount.get(i).getBaseEmoji().equals(oldSelection.getBaseEmoji())) {
|
||||
newPosition = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (newPosition == -1 && !newEmojiCount.isEmpty()) {
|
||||
selectedPosition = 0;
|
||||
onEmojiCountSelectedListener.onSelected(null);
|
||||
} else {
|
||||
selectedPosition = newPosition + 1;
|
||||
}
|
||||
} else if (!newEmojiCount.isEmpty()) {
|
||||
selectedPosition = 0;
|
||||
onEmojiCountSelectedListener.onSelected(null);
|
||||
}
|
||||
|
||||
this.emojiCountList = newEmojiCount;
|
||||
|
||||
this.totalCount = Stream.of(emojiCountList).reduce(0, (sum, e) -> sum + e.getCount());
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_emoji_item, parent, false), position -> {
|
||||
if (position != -1 && position != selectedPosition) {
|
||||
onEmojiCountSelectedListener.onSelected(position == 0 ? null : emojiCountList.get(position - 1).getBaseEmoji());
|
||||
|
||||
int oldPosition = selectedPosition;
|
||||
selectedPosition = position;
|
||||
|
||||
notifyItemChanged(oldPosition);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
if (position == 0) {
|
||||
holder.bind(null, totalCount, selectedPosition == position);
|
||||
} else {
|
||||
EmojiCount item = emojiCountList.get(position - 1);
|
||||
holder.bind(item.getDisplayEmoji(), item.getCount(), selectedPosition == position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 1 + emojiCountList.size();
|
||||
}
|
||||
|
||||
static final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final Drawable selectedBackground;
|
||||
private final EmojiTextView emojiView;
|
||||
private final TextView countView;
|
||||
|
||||
ViewHolder(@NonNull View itemView, @NonNull OnViewHolderClickListener onClickListener) {
|
||||
super(itemView);
|
||||
emojiView = itemView.findViewById(R.id.reactions_bottom_view_emoji_item_emoji);
|
||||
countView = itemView.findViewById(R.id.reactions_bottom_view_emoji_item_text );
|
||||
selectedBackground = ThemeUtil.getThemedDrawable(itemView.getContext(), R.attr.reactions_bottom_dialog_fragment_emoji_selected);
|
||||
|
||||
itemView.setOnClickListener(v -> onClickListener.onClick(getAdapterPosition()));
|
||||
}
|
||||
|
||||
void bind(@Nullable String emoji, int count, boolean selected) {
|
||||
if (emoji != null) {
|
||||
emojiView.setVisibility(View.VISIBLE);
|
||||
emojiView.setText(emoji);
|
||||
countView.setText(String.valueOf(count));
|
||||
} else {
|
||||
emojiView.setVisibility(View.GONE);
|
||||
countView.setText(itemView.getContext().getString(R.string.ReactionsBottomSheetDialogFragment_all, count));
|
||||
}
|
||||
itemView.setBackground(selected ? selectedBackground : null);
|
||||
}
|
||||
}
|
||||
|
||||
interface OnViewHolderClickListener {
|
||||
void onClick(int position);
|
||||
}
|
||||
|
||||
interface OnEmojiCountSelectedListener {
|
||||
void onSelected(@Nullable String emoji);
|
||||
}
|
||||
}
|
|
@ -11,7 +11,6 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsLoader.Reaction;
|
||||
import org.thoughtcrime.securesms.util.AvatarUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
|
@ -19,9 +18,9 @@ import java.util.List;
|
|||
|
||||
final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecipientsAdapter.ViewHolder> {
|
||||
|
||||
private List<Reaction> data = Collections.emptyList();
|
||||
private List<ReactionDetails> data = Collections.emptyList();
|
||||
|
||||
public void updateData(List<Reaction> newData) {
|
||||
public void updateData(List<ReactionDetails> newData) {
|
||||
data = newData;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
@ -44,7 +43,7 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
|
|||
return data.size();
|
||||
}
|
||||
|
||||
final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
static final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView recipient;
|
||||
|
@ -58,7 +57,7 @@ final class ReactionRecipientsAdapter extends RecyclerView.Adapter<ReactionRecip
|
|||
emoji = itemView.findViewById(R.id.reactions_bottom_view_recipient_emoji);
|
||||
}
|
||||
|
||||
void bind(@NonNull Reaction reaction) {
|
||||
void bind(@NonNull ReactionDetails reaction) {
|
||||
this.emoji.setText(reaction.getDisplayEmoji());
|
||||
|
||||
if (reaction.getSender().isLocalNumber()) {
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.adapter.AlwaysChangedDiffUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ReactionViewPagerAdapter provides pages to a ViewPager2 which contains the reactions on a given message.
|
||||
*/
|
||||
class ReactionViewPagerAdapter extends ListAdapter<EmojiCount, ReactionViewPagerAdapter.ViewHolder> {
|
||||
|
||||
private int selectedPosition = 0;
|
||||
|
||||
protected ReactionViewPagerAdapter() {
|
||||
super(new AlwaysChangedDiffUtil<>());
|
||||
}
|
||||
|
||||
@NonNull EmojiCount getEmojiCount(int position) {
|
||||
return getItem(position);
|
||||
}
|
||||
|
||||
void enableNestedScrollingForPosition(int position) {
|
||||
selectedPosition = position;
|
||||
|
||||
notifyItemRangeChanged(0, getItemCount(), new Object());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_recycler, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position);
|
||||
} else {
|
||||
holder.setSelected(selectedPosition);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.onBind(getItem(position));
|
||||
holder.setSelected(selectedPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||
recyclerView.setNestedScrollingEnabled(false);
|
||||
ViewGroup.LayoutParams params = recyclerView.getLayoutParams();
|
||||
params.height = (int) (recyclerView.getResources().getDisplayMetrics().heightPixels * 0.80);
|
||||
recyclerView.setLayoutParams(params);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final RecyclerView recycler;
|
||||
private final ReactionRecipientsAdapter adapter = new ReactionRecipientsAdapter();
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
recycler = (RecyclerView) itemView;
|
||||
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
|
||||
recycler.setLayoutParams(params);
|
||||
recycler.setAdapter(adapter);
|
||||
}
|
||||
|
||||
public void onBind(@NonNull EmojiCount emojiCount) {
|
||||
adapter.updateData(emojiCount.getReactions());
|
||||
}
|
||||
|
||||
public void setSelected(int position) {
|
||||
recycler.setNestedScrollingEnabled(getAdapterPosition() == position);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,21 +1,32 @@
|
|||
package org.thoughtcrime.securesms.reactions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogFragment {
|
||||
|
||||
|
@ -23,12 +34,11 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
private static final String ARGS_IS_MMS = "reactions.args.is.mms";
|
||||
|
||||
private long messageId;
|
||||
private RecyclerView recipientRecyclerView;
|
||||
private RecyclerView emojiRecyclerView;
|
||||
private ViewPager2 recipientPagerView;
|
||||
private ReactionsLoader reactionsLoader;
|
||||
private ReactionRecipientsAdapter recipientsAdapter;
|
||||
private ReactionEmojiCountAdapter emojiCountAdapter;
|
||||
private ReactionViewPagerAdapter recipientsAdapter;
|
||||
private ReactionsViewModel viewModel;
|
||||
private Callback callback;
|
||||
|
||||
public static DialogFragment create(long messageId, boolean isMms) {
|
||||
Bundle args = new Bundle();
|
||||
|
@ -42,13 +52,20 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
callback = (Callback) context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
|
||||
if (ThemeUtil.isDarkTheme(requireContext())) {
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_BottomSheetDialog_Fixed);
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny);
|
||||
} else {
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_Light_BottomSheetDialog_Fixed);
|
||||
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny);
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -63,16 +80,56 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
recipientRecyclerView = view.findViewById(R.id.reactions_bottom_view_recipient_recycler);
|
||||
emojiRecyclerView = view.findViewById(R.id.reactions_bottom_view_emoji_recycler);
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
emojiRecyclerView.setNestedScrollingEnabled(false);
|
||||
messageId = getArguments().getLong(ARGS_MESSAGE_ID);
|
||||
if (savedInstanceState == null) {
|
||||
FrameLayout container = requireDialog().findViewById(R.id.container);
|
||||
LayoutInflater layoutInflater = LayoutInflater.from(requireContext());
|
||||
View statusBarShader = layoutInflater.inflate(R.layout.react_with_any_emoji_status_fade, container, false);
|
||||
TabLayout emojiTabs = (TabLayout) layoutInflater.inflate(R.layout.reactions_bottom_sheet_dialog_fragment_tabs, container, false);
|
||||
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtil.getStatusBarHeight(container));
|
||||
|
||||
statusBarShader.setLayoutParams(params);
|
||||
|
||||
container.addView(statusBarShader, 0);
|
||||
container.addView(emojiTabs);
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(container, (v, insets) -> insets.consumeSystemWindowInsets());
|
||||
|
||||
new TabLayoutMediator(emojiTabs, recipientPagerView, (tab, position) -> {
|
||||
tab.setCustomView(R.layout.reactions_bottom_sheet_dialog_fragment_emoji_item);
|
||||
|
||||
View customView = Objects.requireNonNull(tab.getCustomView());
|
||||
EmojiImageView emoji = customView.findViewById(R.id.reactions_bottom_view_emoji_item_emoji);
|
||||
TextView text = customView.findViewById(R.id.reactions_bottom_view_emoji_item_text);
|
||||
EmojiCount emojiCount = recipientsAdapter.getEmojiCount(position);
|
||||
|
||||
if (position != 0) {
|
||||
emoji.setVisibility(View.VISIBLE);
|
||||
emoji.setImageEmoji(emojiCount.getDisplayEmoji());
|
||||
text.setText(String.valueOf(emojiCount.getCount()));
|
||||
} else {
|
||||
emoji.setVisibility(View.GONE);
|
||||
text.setText(requireContext().getString(R.string.ReactionsBottomSheetDialogFragment_all, emojiCount.getCount()));
|
||||
}
|
||||
}).attach();
|
||||
}
|
||||
|
||||
setUpViewModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
recipientPagerView = view.findViewById(R.id.reactions_bottom_view_recipient_pager);
|
||||
messageId = requireArguments().getLong(ARGS_MESSAGE_ID);
|
||||
|
||||
setUpRecipientsRecyclerView();
|
||||
setUpEmojiRecyclerView();
|
||||
setUpViewModel();
|
||||
|
||||
reactionsLoader = new ReactionsLoader(requireContext(),
|
||||
requireArguments().getLong(ARGS_MESSAGE_ID),
|
||||
requireArguments().getBoolean(ARGS_IS_MMS));
|
||||
|
||||
LoaderManager.getInstance(requireActivity()).initLoader((int) messageId, null, reactionsLoader);
|
||||
}
|
||||
|
@ -83,34 +140,48 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
|
|||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void setUpRecipientsRecyclerView() {
|
||||
recipientsAdapter = new ReactionRecipientsAdapter();
|
||||
recipientRecyclerView.setAdapter(recipientsAdapter);
|
||||
@Override
|
||||
public void onDismiss(@NonNull DialogInterface dialog) {
|
||||
super.onDismiss(dialog);
|
||||
|
||||
callback.onReactionsDialogDismissed();
|
||||
}
|
||||
|
||||
private void setUpEmojiRecyclerView() {
|
||||
emojiCountAdapter = new ReactionEmojiCountAdapter((emoji -> viewModel.setFilterEmoji(emoji)));
|
||||
emojiRecyclerView.setAdapter(emojiCountAdapter);
|
||||
private void setUpRecipientsRecyclerView() {
|
||||
recipientsAdapter = new ReactionViewPagerAdapter();
|
||||
|
||||
recipientPagerView.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
recipientPagerView.post(() -> {
|
||||
recipientsAdapter.enableNestedScrollingForPosition(position);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
if (state == ViewPager2.SCROLL_STATE_IDLE) {
|
||||
recipientPagerView.requestLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
recipientPagerView.setAdapter(recipientsAdapter);
|
||||
}
|
||||
|
||||
private void setUpViewModel() {
|
||||
reactionsLoader = new ReactionsLoader(requireContext(),
|
||||
getArguments().getLong(ARGS_MESSAGE_ID),
|
||||
getArguments().getBoolean(ARGS_IS_MMS));
|
||||
|
||||
ReactionsViewModel.Factory factory = new ReactionsViewModel.Factory(reactionsLoader);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ReactionsViewModel.class);
|
||||
|
||||
viewModel.getRecipients().observe(getViewLifecycleOwner(), reactions -> {
|
||||
if (reactions.size() == 0) dismiss();
|
||||
|
||||
recipientsAdapter.updateData(reactions);
|
||||
});
|
||||
|
||||
viewModel.getEmojiCounts().observe(getViewLifecycleOwner(), emojiCounts -> {
|
||||
if (emojiCounts.size() == 0) dismiss();
|
||||
if (emojiCounts.size() <= 1) dismiss();
|
||||
|
||||
emojiCountAdapter.updateData(emojiCounts);
|
||||
recipientsAdapter.submitList(emojiCounts);
|
||||
});
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onReactionsDialogDismissed();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderMan
|
|||
private final boolean isMms;
|
||||
private final Context appContext;
|
||||
|
||||
private MutableLiveData<List<Reaction>> internalLiveData = new MutableLiveData<>();
|
||||
private MutableLiveData<List<ReactionDetails>> internalLiveData = new MutableLiveData<>();
|
||||
|
||||
public ReactionsLoader(@NonNull Context context, long messageId, boolean isMms)
|
||||
{
|
||||
|
@ -47,6 +47,8 @@ public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderMan
|
|||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
data.moveToPosition(-1);
|
||||
|
||||
MessageRecord record = isMms ? DatabaseFactory.getMmsDatabase(appContext).readerFor(data).getNext()
|
||||
: DatabaseFactory.getSmsDatabase(appContext).readerFor(data).getNext();
|
||||
|
||||
|
@ -54,10 +56,10 @@ public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderMan
|
|||
internalLiveData.postValue(Collections.emptyList());
|
||||
} else {
|
||||
internalLiveData.postValue(Stream.of(record.getReactions())
|
||||
.map(reactionRecord -> new Reaction(Recipient.resolved(reactionRecord.getAuthor()),
|
||||
EmojiUtil.getCanonicalRepresentation(reactionRecord.getEmoji()),
|
||||
reactionRecord.getEmoji(),
|
||||
reactionRecord.getDateReceived()))
|
||||
.map(reactionRecord -> new ReactionDetails(Recipient.resolved(reactionRecord.getAuthor()),
|
||||
EmojiUtil.getCanonicalRepresentation(reactionRecord.getEmoji()),
|
||||
reactionRecord.getEmoji(),
|
||||
reactionRecord.getDateReceived()))
|
||||
.toList());
|
||||
}
|
||||
});
|
||||
|
@ -69,7 +71,7 @@ public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderMan
|
|||
}
|
||||
|
||||
@Override
|
||||
public LiveData<List<Reaction>> getReactions() {
|
||||
public LiveData<List<ReactionDetails>> getReactions() {
|
||||
return internalLiveData;
|
||||
}
|
||||
|
||||
|
@ -103,33 +105,4 @@ public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderMan
|
|||
}
|
||||
}
|
||||
|
||||
static class Reaction {
|
||||
private final Recipient sender;
|
||||
private final String baseEmoji;
|
||||
private final String displayEmoji;
|
||||
private final long timestamp;
|
||||
|
||||
private Reaction(@NonNull Recipient sender, @NonNull String baseEmoji, @NonNull String displayEmoji, long timestamp) {
|
||||
this.sender = sender;
|
||||
this.baseEmoji = baseEmoji;
|
||||
this.displayEmoji = displayEmoji;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public @NonNull String getBaseEmoji() {
|
||||
return baseEmoji;
|
||||
}
|
||||
|
||||
public @NonNull String getDisplayEmoji() {
|
||||
return displayEmoji;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.reactions;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
@ -12,39 +11,32 @@ import com.annimon.stream.Stream;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.thoughtcrime.securesms.reactions.ReactionsLoader.*;
|
||||
|
||||
public class ReactionsViewModel extends ViewModel {
|
||||
|
||||
private final Repository repository;
|
||||
private final MutableLiveData<String> filterEmoji = new MutableLiveData<>();
|
||||
|
||||
public ReactionsViewModel(@NonNull Repository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public @NonNull LiveData<List<Reaction>> getRecipients() {
|
||||
return Transformations.switchMap(filterEmoji,
|
||||
emoji -> Transformations.map(repository.getReactions(),
|
||||
reactions -> Stream.of(reactions)
|
||||
.filter(reaction -> emoji == null || reaction.getBaseEmoji().equals(emoji))
|
||||
.toList()));
|
||||
}
|
||||
|
||||
public @NonNull LiveData<List<EmojiCount>> getEmojiCounts() {
|
||||
return Transformations.map(repository.getReactions(),
|
||||
reactionList -> Stream.of(reactionList)
|
||||
.groupBy(Reaction::getBaseEmoji)
|
||||
.sorted(this::compareReactions)
|
||||
.map(entry -> new EmojiCount(entry.getKey(), getCountDisplayEmoji(entry.getValue()), entry.getValue().size()))
|
||||
.toList());
|
||||
reactionList -> {
|
||||
List<EmojiCount> emojiCounts = Stream.of(reactionList)
|
||||
.groupBy(ReactionDetails::getBaseEmoji)
|
||||
.sorted(this::compareReactions)
|
||||
.map(entry -> new EmojiCount(entry.getKey(),
|
||||
getCountDisplayEmoji(entry.getValue()),
|
||||
entry.getValue()))
|
||||
.toList();
|
||||
|
||||
emojiCounts.add(0, EmojiCount.all(reactionList));
|
||||
|
||||
return emojiCounts;
|
||||
});
|
||||
}
|
||||
|
||||
public void setFilterEmoji(String filterEmoji) {
|
||||
this.filterEmoji.setValue(filterEmoji);
|
||||
}
|
||||
|
||||
private int compareReactions(@NonNull Map.Entry<String, List<Reaction>> lhs, @NonNull Map.Entry<String, List<Reaction>> rhs) {
|
||||
private int compareReactions(@NonNull Map.Entry<String, List<ReactionDetails>> lhs, @NonNull Map.Entry<String, List<ReactionDetails>> rhs) {
|
||||
int lengthComparison = -Integer.compare(lhs.getValue().size(), rhs.getValue().size());
|
||||
if (lengthComparison != 0) return lengthComparison;
|
||||
|
||||
|
@ -54,15 +46,15 @@ public class ReactionsViewModel extends ViewModel {
|
|||
return -Long.compare(latestTimestampLhs, latestTimestampRhs);
|
||||
}
|
||||
|
||||
private long getLatestTimestamp(List<Reaction> reactions) {
|
||||
private long getLatestTimestamp(List<ReactionDetails> reactions) {
|
||||
return Stream.of(reactions)
|
||||
.max((a, b) -> Long.compare(a.getTimestamp(), b.getTimestamp()))
|
||||
.map(Reaction::getTimestamp)
|
||||
.map(ReactionDetails::getTimestamp)
|
||||
.orElse(-1L);
|
||||
}
|
||||
|
||||
private @NonNull String getCountDisplayEmoji(@NonNull List<Reaction> reactions) {
|
||||
for (Reaction reaction : reactions) {
|
||||
private @NonNull String getCountDisplayEmoji(@NonNull List<ReactionDetails> reactions) {
|
||||
for (ReactionDetails reaction : reactions) {
|
||||
if (reaction.getSender().isLocalNumber()) {
|
||||
return reaction.getDisplayEmoji();
|
||||
}
|
||||
|
@ -72,7 +64,7 @@ public class ReactionsViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
interface Repository {
|
||||
LiveData<List<Reaction>> getReactions();
|
||||
LiveData<List<ReactionDetails>> getReactions();
|
||||
}
|
||||
|
||||
static final class Factory implements ViewModelProvider.Factory {
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel;
|
|||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -30,6 +31,7 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
void onEmojiSelected(@NonNull String emoji) {
|
||||
SignalStore.emojiValues().setPreferredVariation(emoji);
|
||||
repository.addEmojiToMessage(emoji, messageId, isMms);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/core_white" />
|
||||
<corners android:radius="30dp" />
|
||||
</shape>
|
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
|
||||
android:fillColor="@color/core_grey_05"/>
|
||||
<path
|
||||
android:pathData="M8,16m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
|
||||
android:fillColor="@color/core_grey_45"/>
|
||||
<path
|
||||
android:pathData="M16,16m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
|
||||
android:fillColor="@color/core_grey_45"/>
|
||||
<path
|
||||
android:pathData="M24,16m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
|
||||
android:fillColor="@color/core_grey_45"/>
|
||||
</vector>
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?reactions_overlay_toolbar_icon_tint"
|
||||
android:pathData="M20.5,4.5l-1,-1l-7.5,7.4l-7.5,-7.4l-1,1l7.4,7.5l-7.4,7.5l1,1l7.5,-7.4l7.5,7.4l1,-1l-7.4,-7.5z"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/core_grey_05" />
|
||||
</shape>
|
|
@ -3,11 +3,11 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="@color/action_mode_status_bar"
|
||||
android:theme="@style/TextSecure.DarkActionBar.Conversation"
|
||||
android:background="?attr/reactions_overlay_toolbar_background_color"
|
||||
android:theme="@style/TextSecure.DarkActionBar.ReactionOverlay"
|
||||
app:contentInsetStart="0dp"
|
||||
app:contentInsetStartWithNavigation="48sp"
|
||||
app:menu="@menu/conversation_reactions_long_press_menu"
|
||||
app:navigationIcon="@drawable/ic_x_conversation">
|
||||
app:navigationIcon="@drawable/ic_x_tinted">
|
||||
|
||||
</org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar>
|
|
@ -27,9 +27,9 @@
|
|||
android:layout_marginTop="40dp"
|
||||
android:layout_marginBottom="40dp"
|
||||
android:alpha="0"
|
||||
tools:alpha="1"
|
||||
android:background="?reactions_overlay_scrubber_background"
|
||||
android:elevation="4dp" />
|
||||
android:elevation="4dp"
|
||||
tools:alpha="1" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/conversation_reaction_scrubber_foreground"
|
||||
|
|
|
@ -1,30 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/reactions_bottom_view_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:minHeight="340dp"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/react_with_any_emoji_bottom_sheet_dialog_fragment_tabs_height">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/reactions_bottom_view_emoji_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/reactions_bottom_sheet_dialog_fragment_emoji_item" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/reactions_bottom_view_recipient_recycler"
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/reactions_bottom_view_recipient_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/reactions_bottom_sheet_dialog_fragment_recipient_item" />
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="62dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="8dp"
|
||||
|
@ -11,18 +11,14 @@
|
|||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiImageView
|
||||
android:id="@+id/reactions_bottom_view_emoji_item_emoji"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:textSize="22dp"
|
||||
android:textStyle="bold"
|
||||
app:emoji_forceCustom="true"
|
||||
tools:ignore="SpUsage"
|
||||
tools:text=":-)" />
|
||||
app:emoji_forceCustom="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reactions_bottom_view_emoji_item_text"
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/reactions_bottom_view_recipient_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/reactions_bottom_sheet_dialog_fragment_recipient_item" />
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.tabs.TabLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/emoji_tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/react_with_any_emoji_bottom_sheet_dialog_fragment_tabs_height"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?attr/emoji_tab_strip_background"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tabIndicatorColor="?colorAccent"
|
||||
app:tabMinWidth="48dp"
|
||||
app:tabMode="scrollable"
|
||||
app:tabPadding="0dp"
|
||||
app:tabPaddingBottom="0dp"
|
||||
app:tabPaddingEnd="0dp"
|
||||
app:tabPaddingStart="0dp"
|
||||
app:tabPaddingTop="0dp" />
|
|
@ -5,36 +5,43 @@
|
|||
android:id="@+id/action_info"
|
||||
android:icon="?menu_info_icon"
|
||||
android:title="@string/conversation_context__menu_message_details"
|
||||
app:iconTint="?attr/reactions_overlay_toolbar_icon_tint"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:icon="?menu_trash_icon"
|
||||
android:title="@string/conversation_context__menu_delete_message"
|
||||
app:iconTint="?attr/reactions_overlay_toolbar_icon_tint"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_copy"
|
||||
android:icon="?menu_copy_icon"
|
||||
android:title="@string/conversation_context__menu_copy_text"
|
||||
app:iconTint="?attr/reactions_overlay_toolbar_icon_tint"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_reply"
|
||||
android:icon="?menu_reply_icon"
|
||||
android:title="@string/conversation_context__menu_reply_to_message"
|
||||
app:iconTint="?attr/reactions_overlay_toolbar_icon_tint"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:visible="false"
|
||||
android:id="@+id/action_download"
|
||||
android:icon="?menu_save_icon"
|
||||
android:title="@string/conversation_context_image__save_attachment"
|
||||
app:iconTint="?attr/reactions_overlay_toolbar_icon_tint"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_multiselect"
|
||||
android:icon="?menu_multi_select_icon"
|
||||
android:title="@string/conversation_context__reaction_multi_select"
|
||||
app:iconTint="?attr/reactions_overlay_toolbar_icon_tint"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_forward"
|
||||
android:icon="?menu_forward_icon"
|
||||
android:title="@string/conversation_context__menu_forward_message"
|
||||
app:iconTint="?attr/reactions_overlay_toolbar_icon_tint"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
|
@ -107,6 +107,9 @@
|
|||
<attr name="emoji_category_emoticons" format="reference"/>
|
||||
<attr name="emoji_variation_selector_background" format="reference|color" />
|
||||
|
||||
<attr name="reactions_overlay_toolbar_icon_tint" format="color" />
|
||||
<attr name="reactions_overlay_toolbar_background_color" format="color" />
|
||||
<attr name="reactions_overlay_custom_emoji_icon" format="reference" />
|
||||
<attr name="reactions_overlay_old_background" format="reference" />
|
||||
<attr name="reactions_overlay_scrubber_background" format="reference" />
|
||||
<attr name="reactions_bottom_dialog_fragment_emoji_selected" format="reference" />
|
||||
|
|
|
@ -109,6 +109,21 @@
|
|||
<item name="android:colorControlActivated" tools:ignore="NewApi">@color/core_ultramarine</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.DarkActionBar.ReactionOverlay">
|
||||
<item name="android:popupTheme" tools:ignore="NewApi">?conversation_popup_theme</item>
|
||||
<item name="popupTheme">?conversation_popup_theme</item>
|
||||
<item name="titleTextStyle">@style/TextSecure.TitleTextStyle</item>
|
||||
<item name="subtitleTextStyle">@style/TextSecure.SubtitleTextStyle</item>
|
||||
<item name="actionOverflowButtonStyle">@style/Signal.Toolbar.Overflow</item>
|
||||
<item name="android:actionOverflowButtonStyle">@style/Signal.Toolbar.Overflow</item>
|
||||
<item name="colorControlHighlight">?colorAccent</item>
|
||||
<item name="android:colorControlHighlight" tools:ignore="NewApi">?colorAccent</item>
|
||||
<item name="colorControlNormal">?title_text_color_secondary</item>
|
||||
<item name="android:colorControlNormal" tools:ignore="NewApi">?title_text_color_primary</item>
|
||||
<item name="colorControlActivated">?colorAccent</item>
|
||||
<item name="android:colorControlActivated" tools:ignore="NewApi">?colorAccent</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.TitleTextStyle" parent="TextAppearance.AppCompat.Widget.ActionBar.Title">
|
||||
<item name="android:textColor">?attr/title_text_color_primary</item>
|
||||
<item name="android:textColorHint">?attr/title_text_color_secondary</item>
|
||||
|
|
|
@ -296,8 +296,11 @@
|
|||
<item name="emoji_category_emoticons">@drawable/ic_emoji_emoticon_light_20</item>
|
||||
<item name="emoji_variation_selector_background">@drawable/emoji_variation_selector_background_light</item>
|
||||
|
||||
<item name="reactions_overlay_old_background">@drawable/reactions_old_background_dark</item>
|
||||
<item name="reactions_overlay_scrubber_background">@drawable/conversation_reaction_overlay_background_dark</item>
|
||||
<item name="reactions_overlay_toolbar_icon_tint">?icon_tint</item>
|
||||
<item name="reactions_overlay_toolbar_background_color">@color/core_white</item>
|
||||
<item name="reactions_overlay_custom_emoji_icon">@drawable/ic_any_emoji_32_light</item>
|
||||
<item name="reactions_overlay_old_background">@drawable/reactions_old_background_light</item>
|
||||
<item name="reactions_overlay_scrubber_background">@drawable/conversation_reaction_overlay_background_light</item>
|
||||
<item name="reactions_bottom_dialog_fragment_emoji_selected">@drawable/reactions_bottom_sheet_dialog_fragment_emoji_item_selected_light</item>
|
||||
<item name="reactions_pill_background">@drawable/reaction_pill_background_light</item>
|
||||
<item name="reactions_pill_selected_background">@drawable/reaction_pill_background_selected_light</item>
|
||||
|
@ -615,6 +618,9 @@
|
|||
<item name="conversation_scroll_to_bottom_background">@drawable/scroll_to_bottom_background_dark</item>
|
||||
<item name="conversation_scroll_to_bottom_foreground_color">@color/core_white</item>
|
||||
|
||||
<item name="reactions_overlay_toolbar_icon_tint">@color/core_white</item>
|
||||
<item name="reactions_overlay_toolbar_background_color">@color/action_mode_status_bar</item>
|
||||
<item name="reactions_overlay_custom_emoji_icon">@drawable/ic_any_emoji_32_dark</item>
|
||||
<item name="reactions_overlay_old_background">@drawable/reactions_old_background_dark</item>
|
||||
<item name="reactions_overlay_scrubber_background">@drawable/conversation_reaction_overlay_background_dark</item>
|
||||
<item name="reactions_bottom_dialog_fragment_emoji_selected">@drawable/reactions_bottom_sheet_dialog_fragment_emoji_item_selected_dark</item>
|
||||
|
|
Ładowanie…
Reference in New Issue