Implement ability to react with any emoji behind a flag.

This commit is contained in:
Alex Hart
2020-05-05 14:53:57 -03:00
parent 40b5339ef8
commit d94fc4bc13
53 changed files with 761 additions and 52 deletions

View File

@@ -133,7 +133,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
@Override
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener);
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener, true);
page.setModel(pages.get(position));
container.addView(page);
return page;

View File

@@ -26,7 +26,8 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
public EmojiPageView(@NonNull Context context,
@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener)
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations)
{
super(context);
final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true);
@@ -40,7 +41,8 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
adapter = new EmojiPageViewGridAdapter(EmojiProvider.getInstance(context),
popup,
emojiSelectionListener,
this);
this,
allowVariations);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
@@ -83,6 +85,10 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
}
}
public void setRecyclerNestedScrollingEnabled(boolean enabled) {
recyclerView.setNestedScrollingEnabled(enabled);
}
private static class ScrollDisabler implements RecyclerView.OnItemTouchListener {
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {

View File

@@ -22,17 +22,20 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
private final EmojiVariationSelectorPopup popup;
private final VariationSelectorListener variationSelectorListener;
private final EmojiEventListener emojiEventListener;
private final boolean allowVariations;
public EmojiPageViewGridAdapter(@NonNull EmojiProvider emojiProvider,
@NonNull EmojiVariationSelectorPopup popup,
@NonNull EmojiEventListener emojiEventListener,
@NonNull VariationSelectorListener variationSelectorListener)
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations)
{
this.emojiList = new ArrayList<>();
this.emojiProvider = emojiProvider;
this.popup = popup;
this.emojiEventListener = emojiEventListener;
this.variationSelectorListener = variationSelectorListener;
this.allowVariations = allowVariations;
popup.setOnDismissListener(this);
}
@@ -65,7 +68,7 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
emojiEventListener.onEmojiSelected(emoji.getValue());
});
if (emoji.getVariations().size() > 1) {
if (allowVariations && emoji.getVariations().size() > 1) {
viewHolder.itemView.setOnLongClickListener(v -> {
popup.dismiss();
popup.setVariations(emoji.getVariations());

View File

@@ -6,6 +6,7 @@ import org.whispersystems.libsignal.util.Pair;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -36,6 +37,10 @@ public final class EmojiUtil {
private EmojiUtil() {}
public static List<EmojiPageModel> getDisplayPages() {
return EmojiPages.DISPLAY_PAGES;
}
/**
* This will return all ways we know of expressing a singular emoji. This is to aid in search,
* where some platforms may send an emoji we've locally marked as 'obsolete'.

View File

@@ -197,6 +197,7 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDialogFragment;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter;
@@ -269,7 +270,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
ComposeText.CursorPositionChangedListener,
ConversationSearchBottomBar.EventListener,
StickerKeyboardProvider.StickerEventListener,
AttachmentKeyboard.Callback
AttachmentKeyboard.Callback,
ConversationReactionOverlay.OnReactionSelectedListener,
ReactWithAnyEmojiBottomSheetDialogFragment.Callback
{
private static final int SHORTCUT_ICON_SIZE = Build.VERSION.SDK_INT >= 26 ? ViewUtil.dpToPx(72) : ViewUtil.dpToPx(48 + 16 * 2);
@@ -1714,7 +1717,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
inlineAttachmentButton.setOnClickListener(v -> handleAddAttachment());
reactionOverlay.setOnReactionSelectedListener(this::onReactionSelected);
reactionOverlay.setOnReactionSelectedListener(this);
}
protected void initializeActionBar() {
@@ -1831,10 +1834,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
.show(TooltipPopup.POSITION_ABOVE);
}
private void onReactionSelected(MessageRecord messageRecord, String emoji) {
@Override
public void onReactionSelected(MessageRecord messageRecord, String emoji) {
final Context context = getApplicationContext();
reactionOverlay.hide();
SignalExecutors.BOUNDED.execute(() -> {
ReactionRecord oldRecord = Stream.of(messageRecord.getReactions())
.filter(record -> record.getAuthor().equals(Recipient.self().getId()))
@@ -1849,6 +1854,35 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
});
}
@Override
public void onCustomReactionSelected(@NonNull MessageRecord messageRecord, boolean hasAddedCustomEmoji) {
ReactionRecord oldRecord = Stream.of(messageRecord.getReactions())
.filter(record -> record.getAuthor().equals(Recipient.self().getId()))
.findFirst()
.orElse(null);
if (oldRecord != null && hasAddedCustomEmoji) {
final Context context = getApplicationContext();
reactionOverlay.hide();
SignalExecutors.BOUNDED.execute(() -> MessageSender.sendReactionRemoval(context,
messageRecord.getId(),
messageRecord.isMms(),
oldRecord));
} else {
reactionOverlay.hideAllButMask();
ReactWithAnyEmojiBottomSheetDialogFragment.createForMessageRecord(messageRecord)
.show(getSupportFragmentManager(), "BOTTOM");
}
}
@Override
public void onReactWithAnyEmojiDialogDismissed() {
reactionOverlay.hideMask();
}
@Override
public void onSearchMoveUpPressed() {
searchViewModel.onMoveUp();

View File

@@ -19,6 +19,7 @@ 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;
@@ -30,14 +31,17 @@ import com.annimon.stream.Stream;
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.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
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;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public final class ConversationReactionOverlay extends RelativeLayout {
@@ -60,12 +64,13 @@ public final class ConversationReactionOverlay extends RelativeLayout {
private boolean downIsOurs;
private boolean isToolbarTouch;
private int selected = -1;
private int customEmojiIndex;
private int originalStatusBarColor;
private View backgroundView;
private ConstraintLayout foregroundView;
private View selectedView;
private View[] emojiViews;
private EmojiImageView[] emojiViews;
private MaskView maskView;
private Toolbar toolbar;
@@ -85,8 +90,10 @@ public final class ConversationReactionOverlay extends RelativeLayout {
private Toolbar.OnMenuItemClickListener onToolbarItemClickedListener;
private OnHideListener onHideListener;
private AnimatorSet revealAnimatorSet = new AnimatorSet();
private AnimatorSet hideAnimatorSet = new AnimatorSet();
private AnimatorSet revealAnimatorSet = new AnimatorSet();
private AnimatorSet hideAnimatorSet = new AnimatorSet();
private AnimatorSet hideAllButMaskAnimatorSet = new AnimatorSet();
private AnimatorSet hideMaskAnimatorSet = new AnimatorSet();
public ConversationReactionOverlay(@NonNull Context context) {
super(context);
@@ -111,7 +118,10 @@ public final class ConversationReactionOverlay extends RelativeLayout {
emojiViews = Stream.of(ReactionEmoji.values())
.map(e -> findViewById(e.viewId))
.toArray(View[]::new);
.toArray(EmojiImageView[]::new);
customEmojiIndex = FeatureFlags.reactWithAnyEmoji() ? ReactionEmoji.values().length - 1
: ReactionEmoji.values().length;
distanceFromTouchDownPointToTopOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_top);
distanceFromTouchDownPointToBottomOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_bottom);
@@ -144,7 +154,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
selected = -1;
setupToolbarMenuItems();
setupSelectedEmojiBackground();
setupSelectedEmoji();
if (Build.VERSION.SDK_INT >= 21) {
View statusBarBackground = activity.findViewById(android.R.id.statusBarBackground);
@@ -188,6 +198,22 @@ public final class ConversationReactionOverlay extends RelativeLayout {
public void hide() {
maskView.setTarget(null);
hideInternal(hideAnimatorSet, onHideListener);
}
public void hideAllButMask() {
hideInternal(hideAllButMaskAnimatorSet, null);
}
public void hideMask() {
hideMaskAnimatorSet.start();
if (onHideListener != null) {
onHideListener.onHide();
}
}
private void hideInternal(@NonNull AnimatorSet hideAnimatorSet, @Nullable OnHideListener onHideListener) {
overlayState = OverlayState.HIDDEN;
revealAnimatorSet.end();
@@ -316,20 +342,30 @@ public final class ConversationReactionOverlay extends RelativeLayout {
}
}
private void setupSelectedEmojiBackground() {
private void setupSelectedEmoji() {
final String oldEmoji = getOldEmoji(messageRecord);
if (oldEmoji == null) {
selectedView.setVisibility(View.GONE);
}
boolean foundSelected = false;
for (int i = 0; i < emojiViews.length; i++) {
final View view = emojiViews[i];
final EmojiImageView view = emojiViews[i];
view.setScaleX(1.0f);
view.setScaleY(1.0f);
view.setTranslationY(0);
if (ReactionEmoji.values()[i].emoji.equals(oldEmoji)) {
boolean isAtCustomIndex = i == customEmojiIndex;
boolean isNotAtCustomIndexAndOldEmojiMatches = !isAtCustomIndex && ReactionEmoji.values()[i].emoji.equals(oldEmoji);
boolean isAtCustomIndexAndOldEmojiExists = isAtCustomIndex && oldEmoji != null;
if (!foundSelected &&
(isNotAtCustomIndexAndOldEmojiMatches || isAtCustomIndexAndOldEmojiExists))
{
foundSelected = true;
selectedView.setVisibility(View.VISIBLE);
ConstraintSet constraintSet = new ConstraintSet();
@@ -339,6 +375,18 @@ public final class ConversationReactionOverlay extends RelativeLayout {
constraintSet.connect(selectedView.getId(), ConstraintSet.LEFT, view.getId(), ConstraintSet.LEFT);
constraintSet.connect(selectedView.getId(), ConstraintSet.RIGHT, view.getId(), ConstraintSet.RIGHT);
constraintSet.applyTo(foregroundView);
if (isAtCustomIndex) {
view.setImageEmoji(oldEmoji);
view.setTag(oldEmoji);
} else {
view.setImageEmoji(ReactionEmoji.values()[i].emoji);
}
} else if (isAtCustomIndex) {
view.setImageDrawable(AppCompatResources.getDrawable(getContext(), R.drawable.ic_any_emoji_32));
view.setTag(null);
} else {
view.setImageEmoji(ReactionEmoji.values()[i].emoji);
}
}
}
@@ -396,9 +444,14 @@ public final class ConversationReactionOverlay extends RelativeLayout {
}
private void handleUpEvent() {
hide();
if (selected != -1 && onReactionSelectedListener != null) {
onReactionSelectedListener.onReactionSelected(messageRecord, ReactionEmoji.values()[selected].emoji);
if (selected == customEmojiIndex) {
onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].getTag() != null);
} else {
onReactionSelectedListener.onReactionSelected(messageRecord, ReactionEmoji.values()[selected].emoji);
}
} else {
hide();
}
}
@@ -494,7 +547,6 @@ public final class ConversationReactionOverlay extends RelativeLayout {
Animator overlayHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
overlayHideAnim.setTarget(maskView);
overlayHideAnim.setDuration(duration);
hides.add(overlayHideAnim);
Animator backgroundHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out);
backgroundHideAnim.setTarget(backgroundView);
@@ -511,15 +563,26 @@ public final class ConversationReactionOverlay extends RelativeLayout {
toolbarHideAnim.setDuration(duration);
hides.add(toolbarHideAnim);
hideAnimatorSet.addListener(new AnimationCompleteListener() {
AnimationCompleteListener hideListener = new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
setVisibility(View.GONE);
}
});
};
List<Animator> hideAllAnimators = new LinkedList<>(hides);
hideAllAnimators.add(overlayHideAnim);
hideAnimatorSet.addListener(hideListener);
hideAnimatorSet.setInterpolator(INTERPOLATOR);
hideAnimatorSet.playTogether(hides);
hideAnimatorSet.playTogether(hideAllAnimators);
hideAllButMaskAnimatorSet.setInterpolator(INTERPOLATOR);
hideAllButMaskAnimatorSet.playTogether(hides);
hideMaskAnimatorSet.addListener(hideListener);
hideMaskAnimatorSet.setInterpolator(INTERPOLATOR);
hideMaskAnimatorSet.playTogether(overlayHideAnim);
}
public interface OnHideListener {
@@ -528,6 +591,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
public interface OnReactionSelectedListener {
void onReactionSelected(@NonNull MessageRecord messageRecord, String emoji);
void onCustomReactionSelected(@NonNull MessageRecord messageRecord, boolean hasAddedCustomEmoji);
}
private static class Boundary {

View File

@@ -21,7 +21,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.thoughtcrime.securesms.ClearProfileAvatarActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.ArrayList;
@@ -63,8 +62,8 @@ public class AvatarSelectionBottomSheetDialogFragment extends BottomSheetDialogF
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
setStyle(DialogFragment.STYLE_NORMAL,
ThemeUtil.isDarkTheme(requireContext()) ? R.style.Theme_Design_BottomSheetDialog_Fixed
: R.style.Theme_Design_Light_BottomSheetDialog_Fixed);
ThemeUtil.isDarkTheme(requireContext()) ? R.style.Theme_Signal_BottomSheetDialog_Fixed
: R.style.Theme_Signal_Light_BottomSheetDialog_Fixed);
super.onCreate(savedInstanceState);

View File

@@ -46,9 +46,9 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF
public void onCreate(@Nullable Bundle savedInstanceState) {
if (ThemeUtil.isDarkTheme(requireContext())) {
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Design_BottomSheetDialog_Fixed);
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_BottomSheetDialog_Fixed);
} else {
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Design_Light_BottomSheetDialog_Fixed);
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_Light_BottomSheetDialog_Fixed);
}
super.onCreate(savedInstanceState);

View File

@@ -0,0 +1,88 @@
package org.thoughtcrime.securesms.reactions.any;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
import org.thoughtcrime.securesms.components.emoji.EmojiPageView;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter;
import java.util.List;
final class ReactWithAnyEmojiAdapter extends RecyclerView.Adapter<ReactWithAnyEmojiAdapter.ViewHolder> {
private final List<EmojiPageModel> models;
private final EmojiKeyboardProvider.EmojiEventListener emojiEventListener;
private final EmojiPageViewGridAdapter.VariationSelectorListener variationSelectorListener;
private final Callbacks callbacks;
ReactWithAnyEmojiAdapter(@NonNull List<EmojiPageModel> models,
@NonNull EmojiKeyboardProvider.EmojiEventListener emojiEventListener,
@NonNull EmojiPageViewGridAdapter.VariationSelectorListener variationSelectorListener,
@NonNull Callbacks callbacks)
{
this.models = models;
this.emojiEventListener = emojiEventListener;
this.variationSelectorListener = variationSelectorListener;
this.callbacks = callbacks;
}
@Override
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(new EmojiPageView(parent.getContext(), emojiEventListener, variationSelectorListener, false));
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
holder.bind(models.get(position));
}
@Override
public int getItemCount() {
return models.size();
}
@Override
public void onViewAttachedToWindow(@NonNull ViewHolder holder) {
callbacks.onViewHolderAttached(holder.getAdapterPosition(), holder.emojiPageView);
}
@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 final class ViewHolder extends RecyclerView.ViewHolder {
private final EmojiPageView emojiPageView;
ViewHolder(@NonNull EmojiPageView itemView) {
super(itemView);
emojiPageView = itemView;
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
emojiPageView.setLayoutParams(params);
}
void bind(@NonNull EmojiPageModel model) {
emojiPageView.setModel(model);
}
}
interface Callbacks {
void onViewHolderAttached(int adapterPosition, EmojiPageView pageView);
}
}

View File

@@ -0,0 +1,268 @@
package org.thoughtcrime.securesms.reactions.any;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.SparseArray;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextSwitcher;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.shape.CornerFamily;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;
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.EmojiKeyboardProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiPageView;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import static org.thoughtcrime.securesms.R.layout.react_with_any_emoji_tab;
public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomSheetDialogFragment implements EmojiKeyboardProvider.EmojiEventListener, EmojiPageViewGridAdapter.VariationSelectorListener {
private static final String ARG_MESSAGE_ID = "arg_message_id";
private static final String ARG_IS_MMS = "arg_is_mms";
private ReactWithAnyEmojiViewModel viewModel;
private TextSwitcher categoryLabel;
private ViewPager2 categoryPager;
private ReactWithAnyEmojiAdapter adapter;
private OnPageChanged onPageChanged;
private SparseArray<EmojiPageView> pageArray = new SparseArray<>();
private Callback callback;
public static DialogFragment createForMessageRecord(@NonNull MessageRecord messageRecord) {
DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment();
Bundle args = new Bundle();
args.putLong(ARG_MESSAGE_ID, messageRecord.getId());
args.putBoolean(ARG_IS_MMS, messageRecord.isMms());
fragment.setArguments(args);
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_ReactWithAny);
} else {
setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny);
}
super.onCreate(savedInstanceState);
}
@Override
public @NonNull Dialog onCreateDialog(Bundle savedInstanceState) {
BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
ShapeAppearanceModel shapeAppearanceModel = ShapeAppearanceModel.builder()
.setTopLeftCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 8))
.setTopRightCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 8))
.build();
MaterialShapeDrawable dialogBackground = new MaterialShapeDrawable(shapeAppearanceModel);
dialogBackground.setTint(ThemeUtil.getThemedColor(requireContext(), R.attr.dialog_background_color));
dialog.getBehavior().addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
ViewCompat.setBackground(bottomSheet, dialogBackground);
}
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
}
});
return dialog;
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
return inflater.inflate(R.layout.react_with_any_emoji_bottom_sheet_dialog_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
initializeViewModel();
categoryLabel = view.findViewById(R.id.category_label);
categoryPager = view.findViewById(R.id.category_pager);
adapter = new ReactWithAnyEmojiAdapter(viewModel.getEmojiPageModels(), this, this, (position, pageView) -> {
pageArray.put(position, pageView);
if (categoryPager.getCurrentItem() == position) {
updateFocusedRecycler(position);
}
});
onPageChanged = new OnPageChanged();
categoryPager.setAdapter(adapter);
categoryPager.registerOnPageChangeCallback(onPageChanged);
int startPateIndex = viewModel.getStartIndex();
categoryPager.setCurrentItem(startPateIndex, false);
presentCategoryLabel(viewModel.getCategoryIconAttr(startPateIndex));
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
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 categoryTabs = (TabLayout) layoutInflater.inflate(R.layout.react_with_any_emoji_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(categoryTabs);
ViewCompat.setOnApplyWindowInsetsListener(container, (v, insets) -> insets.consumeSystemWindowInsets());
new TabLayoutMediator(categoryTabs, categoryPager, (tab, position) -> {
tab.setCustomView(react_with_any_emoji_tab)
.setIcon(ThemeUtil.getThemedDrawable(requireContext(), viewModel.getCategoryIconAttr(position)));
}).attach();
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
categoryPager.unregisterOnPageChangeCallback(onPageChanged);
}
@Override
public void onDismiss(@NonNull DialogInterface dialog) {
super.onDismiss(dialog);
callback.onReactWithAnyEmojiDialogDismissed();
}
private void initializeViewModel() {
Bundle args = requireArguments();
ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext());
ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS));
viewModel = ViewModelProviders.of(this, factory).get(ReactWithAnyEmojiViewModel.class);
}
@Override
public void onEmojiSelected(String emoji) {
viewModel.onEmojiSelected(emoji);
dismiss();
}
@Override
public void onKeyEvent(KeyEvent keyEvent) {
}
@Override
public void onVariationSelectorStateChanged(boolean open) {
}
private void updateFocusedRecycler(int position) {
for (int i = 0; i < pageArray.size(); i++) {
pageArray.valueAt(i).setRecyclerNestedScrollingEnabled(false);
}
EmojiPageView toFocus = pageArray.get(position);
if (toFocus != null) {
toFocus.setRecyclerNestedScrollingEnabled(true);
categoryPager.requestLayout();
}
presentCategoryLabel(viewModel.getCategoryIconAttr(position));
}
private void presentCategoryLabel(@AttrRes int iconAttr) {
switch (iconAttr) {
case R.attr.emoji_category_recent:
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__recently_used));
break;
case R.attr.emoji_category_people:
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people));
break;
case R.attr.emoji_category_nature:
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature));
break;
case R.attr.emoji_category_foods:
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food));
break;
case R.attr.emoji_category_activity:
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities));
break;
case R.attr.emoji_category_places:
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places));
break;
case R.attr.emoji_category_objects:
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects));
break;
case R.attr.emoji_category_symbols:
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols));
break;
case R.attr.emoji_category_flags:
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags));
break;
case R.attr.emoji_category_emoticons:
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons));
break;
default:
throw new AssertionError();
}
}
private class OnPageChanged extends ViewPager2.OnPageChangeCallback {
@Override
public void onPageSelected(int position) {
updateFocusedRecycler(position);
}
}
public interface Callback {
void onReactWithAnyEmojiDialogDismissed();
}
}

View File

@@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.reactions.any;
import android.content.Context;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.LinkedList;
import java.util.List;
final class ReactWithAnyEmojiRepository {
private final Context context;
private final RecentEmojiPageModel recentEmojiPageModel;
private final List<EmojiPageModel> emojiPageModels;
ReactWithAnyEmojiRepository(@NonNull Context context) {
this.context = context;
this.recentEmojiPageModel = new RecentEmojiPageModel(context);
this.emojiPageModels = new LinkedList<>();
emojiPageModels.add(recentEmojiPageModel);
emojiPageModels.addAll(EmojiUtil.getDisplayPages());
emojiPageModels.remove(emojiPageModels.size() - 1);
}
List<EmojiPageModel> getEmojiPageModels() {
return emojiPageModels;
}
void addEmojiToMessage(@NonNull String emoji, long messageId, boolean isMms) {
recentEmojiPageModel.onCodePointSelected(emoji);
SignalExecutors.BOUNDED.execute(() -> MessageSender.sendNewReaction(context, messageId, isMms, emoji));
}
}

View File

@@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.reactions.any;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
import java.util.List;
public final class ReactWithAnyEmojiViewModel extends ViewModel {
private final ReactWithAnyEmojiRepository repository;
private final long messageId;
private final boolean isMms;
private ReactWithAnyEmojiViewModel(@NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) {
this.repository = repository;
this.messageId = messageId;
this.isMms = isMms;
}
List<EmojiPageModel> getEmojiPageModels() {
return repository.getEmojiPageModels();
}
int getStartIndex() {
return repository.getEmojiPageModels().get(0).getEmoji().size() == 0 ? 1 : 0;
}
void onEmojiSelected(@NonNull String emoji) {
repository.addEmojiToMessage(emoji, messageId, isMms);
}
@AttrRes int getCategoryIconAttr(int position) {
return repository.getEmojiPageModels().get(position).getIconAttr();
}
static class Factory implements ViewModelProvider.Factory {
private final ReactWithAnyEmojiRepository repository;
private final long messageId;
private final boolean isMms;
Factory(@NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) {
this.repository = repository;
this.messageId = messageId;
this.isMms = isMms;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ReactWithAnyEmojiViewModel(repository, messageId, isMms));
}
}
}

View File

@@ -59,6 +59,7 @@ public final class FeatureFlags {
private static final String PROFILE_FOR_CALLING = "android.profileForCalling";
private static final String CALLING_PIP = "android.callingPip";
private static final String NEW_GROUP_UI = "android.newGroupUI";
private static final String REACT_WITH_ANY_EMOJI = "android.reactWithAnyEmoji";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -76,7 +77,8 @@ public final class FeatureFlags {
REMOTE_DELETE,
PROFILE_FOR_CALLING,
CALLING_PIP,
NEW_GROUP_UI
NEW_GROUP_UI,
REACT_WITH_ANY_EMOJI
);
/**
@@ -98,7 +100,8 @@ public final class FeatureFlags {
*/
private static final Set<String> HOT_SWAPPABLE = Sets.newHashSet(
PINS_MEGAPHONE_KILL_SWITCH,
ATTACHMENTS_V3
ATTACHMENTS_V3,
REACT_WITH_ANY_EMOJI
);
/**
@@ -247,6 +250,11 @@ public final class FeatureFlags {
return getBoolean(NEW_GROUP_UI, false);
}
/** React with Any Emoji */
public static boolean reactWithAnyEmoji() {
return getBoolean(REACT_WITH_ANY_EMOJI, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);