From 2a1e5e4471692c2223247af6655eb5eefbf19e04 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Thu, 24 Jun 2021 15:14:34 -0400 Subject: [PATCH] Add React With Any Search and update UX. --- .../emoji/CompositeEmojiPageModel.java | 12 +- .../securesms/components/emoji/Emoji.java | 4 + .../components/emoji/EmojiItemDecoration.kt | 51 ++++ .../components/emoji/EmojiPageModel.java | 1 + .../components/emoji/EmojiPageView.java | 145 ++++++--- .../emoji/EmojiPageViewGridAdapter.java | 167 +++++++++-- .../emoji/RecentEmojiPageModel.java | 15 +- .../emoji/StaticEmojiPageModel.java | 34 +-- .../conversation/ConversationActivity.java | 7 +- .../ConversationReactionDelegate.java | 5 +- .../ConversationReactionOverlay.java | 5 +- .../securesms/emoji/EmojiCategory.kt | 1 + .../securesms/emoji/EmojiJsonParser.kt | 2 +- .../securesms/emoji/EmojiSource.kt | 2 +- .../securesms/giph/ui/GiphyActivity.java | 2 +- .../KeyboardPageCategoryIconViewHolder.kt | 5 +- .../EmojiKeyboardPageCategoriesAdapter.kt | 3 +- .../EmojiKeyboardPageCategoryMappingModel.kt | 7 +- .../emoji/EmojiKeyboardPageViewModel.kt | 6 +- .../keyboard/emoji/KeyboardPageSearchView.kt | 26 +- .../emoji/search/EmojiSearchRepository.kt | 13 +- .../emoji/search/EmojiSearchViewModel.kt | 2 +- .../manage/ManageProfileActivity.java | 4 - .../any/ReactWithAnyEmojiAdapter.java | 181 ------------ ...WithAnyEmojiBottomSheetDialogFragment.java | 275 ++++++++++-------- .../reactions/any/ReactWithAnyEmojiPage.java | 4 + .../any/ReactWithAnyEmojiViewModel.java | 134 ++++++++- .../any/ThisMessageEmojiPageModel.java | 6 + .../reactions/any/TopAndBottomShadowHelper.kt | 76 +++++ .../reactions/edit/EditReactionsFragment.kt | 3 - .../securesms/util/InsetItemDecoration.kt | 47 +++ .../securesms/util/MappingAdapter.java | 20 ++ .../securesms/util/MappingModelList.java | 48 +++ .../res/drawable/bottom_toolbar_shadow.xml | 8 + ...board_pager_fragment_category_selected.xml | 2 +- app/src/main/res/drawable/toolbar_shadow.xml | 2 +- .../main/res/layout/emoji_display_item.xml | 49 +--- app/src/main/res/layout/emoji_grid_header.xml | 13 + app/src/main/res/layout/emoji_grid_layout.xml | 7 - .../main/res/layout/emoji_grid_no_results.xml | 8 + .../res/layout/emoji_text_display_item.xml | 16 + .../res/layout/keyboard_pager_search_bar.xml | 1 - ...any_emoji_bottom_sheet_dialog_fragment.xml | 84 +++--- .../react_with_any_emoji_dual_block_item.xml | 24 -- .../res/layout/react_with_any_emoji_tabs.xml | 45 +-- app/src/main/res/values-night/dark_colors.xml | 7 +- app/src/main/res/values/dimens.xml | 2 +- app/src/main/res/values/light_colors.xml | 7 +- app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/themes.xml | 15 +- .../securesms/emoji/EmojiJsonParserTest.kt | 16 +- .../securesms/emoji/EmojiSourceTest.kt | 2 + 52 files changed, 1014 insertions(+), 608 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiItemDecoration.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiAdapter.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/reactions/any/TopAndBottomShadowHelper.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/util/InsetItemDecoration.kt create mode 100644 app/src/main/res/drawable/bottom_toolbar_shadow.xml create mode 100644 app/src/main/res/layout/emoji_grid_header.xml delete mode 100644 app/src/main/res/layout/emoji_grid_layout.xml create mode 100644 app/src/main/res/layout/emoji_grid_no_results.xml create mode 100644 app/src/main/res/layout/emoji_text_display_item.xml delete mode 100644 app/src/main/res/layout/react_with_any_emoji_dual_block_item.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/CompositeEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/CompositeEmojiPageModel.java index 91d6900631..b216fbd4fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/CompositeEmojiPageModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/CompositeEmojiPageModel.java @@ -6,19 +6,25 @@ import androidx.annotation.AttrRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import java.util.Arrays; +import org.thoughtcrime.securesms.util.Util; + import java.util.LinkedList; import java.util.List; public class CompositeEmojiPageModel implements EmojiPageModel { - @AttrRes private final int iconAttr; - @NonNull private final List models; + @AttrRes private final int iconAttr; + @NonNull private final List models; public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull List models) { this.iconAttr = iconAttr; this.models = models; } + @Override + public String getKey() { + return Util.hasItems(models) ? models.get(0).getKey() : ""; + } + public int getIconAttr() { return iconAttr; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emoji.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emoji.java index 294afb27bc..155d541594 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emoji.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emoji.java @@ -22,4 +22,8 @@ public class Emoji { public List getVariations() { return variations; } + + public boolean hasMultipleVariations() { + return variations.size() > 1; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiItemDecoration.kt new file mode 100644 index 0000000000..122cf615c3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiItemDecoration.kt @@ -0,0 +1,51 @@ +package org.thoughtcrime.securesms.components.emoji + +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import androidx.appcompat.widget.AppCompatTextView +import androidx.recyclerview.widget.RecyclerView +import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiModel +import org.thoughtcrime.securesms.util.InsetItemDecoration +import org.thoughtcrime.securesms.util.ViewUtil + +private val EDGE_LENGTH: Int = ViewUtil.dpToPx(7) +private val HORIZONTAL_INSET: Int = ViewUtil.dpToPx(11) +private val VERTICAL_INSET: Int = ViewUtil.dpToPx(8) + +/** + * Use super class to add insets to the emojis and use the [onDrawOver] to draw the variation + * hint if the emoji has more than one variation. + */ +class EmojiItemDecoration(private val allowVariations: Boolean, private val variationsDrawable: Drawable) : InsetItemDecoration(SetInset()) { + + override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + super.onDrawOver(canvas, parent, state) + + val adapter: EmojiPageViewGridAdapter? = parent.adapter as? EmojiPageViewGridAdapter + if (allowVariations && adapter != null) { + for (i in 0 until parent.childCount) { + val child: View = parent.getChildAt(i) + val position: Int = parent.getChildAdapterPosition(child) + if (position >= 0 && position <= adapter.itemCount) { + val model = adapter.currentList[position] + if (model is EmojiModel && model.emoji.hasMultipleVariations()) { + variationsDrawable.setBounds(child.right, child.bottom - EDGE_LENGTH, child.right + EDGE_LENGTH, child.bottom) + variationsDrawable.draw(canvas) + } + } + } + } + } + + private class SetInset : InsetItemDecoration.SetInset() { + override fun setInset(outRect: Rect, view: View, parent: RecyclerView) { + val isFirstHeader = view.javaClass == AppCompatTextView::class.java && getPosition(view, parent) == 0 + outRect.left = HORIZONTAL_INSET + outRect.right = HORIZONTAL_INSET + outRect.top = if (isFirstHeader) 0 else VERTICAL_INSET + outRect.bottom = VERTICAL_INSET + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java index 71ebdb4bdf..1ed6b18c49 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java @@ -7,6 +7,7 @@ import androidx.annotation.Nullable; import java.util.List; public interface EmojiPageModel { + String getKey(); int getIconAttr(); List getEmoji(); List getDisplayEmoji(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java index 3556039d90..93d692f9a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java @@ -1,91 +1,143 @@ package org.thoughtcrime.securesms.components.emoji; import android.content.Context; -import android.view.LayoutInflater; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; -import android.widget.FrameLayout; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.LinearSmoothScroller; import androidx.recyclerview.widget.RecyclerView; -import com.annimon.stream.Stream; - -import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener; +import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiHeader; +import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiNoResultsModel; import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener; +import org.thoughtcrime.securesms.emoji.EmojiCategory; +import org.thoughtcrime.securesms.util.ContextUtil; +import org.thoughtcrime.securesms.util.DrawableUtil; +import org.thoughtcrime.securesms.util.MappingModel; import org.thoughtcrime.securesms.util.MappingModelList; import org.thoughtcrime.securesms.util.ViewUtil; -public class EmojiPageView extends FrameLayout implements VariationSelectorListener { - private static final String TAG = Log.tag(EmojiPageView.class); +import java.util.Optional; + +public class EmojiPageView extends RecyclerView implements VariationSelectorListener { private EmojiPageModel model; private AdapterFactory adapterFactory; - private RecyclerView recyclerView; - private RecyclerView.LayoutManager layoutManager; + private LinearLayoutManager layoutManager; private RecyclerView.OnItemTouchListener scrollDisabler; private VariationSelectorListener variationSelectorListener; private EmojiVariationSelectorPopup popup; + public EmojiPageView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public EmojiPageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public EmojiPageView(@NonNull Context context, @NonNull EmojiEventListener emojiSelectionListener, @NonNull VariationSelectorListener variationSelectorListener, boolean allowVariations) { - this(context, emojiSelectionListener, variationSelectorListener, allowVariations, new GridLayoutManager(context, 8), R.layout.emoji_display_item); + super(context); + initialize(emojiSelectionListener, variationSelectorListener, allowVariations); } public EmojiPageView(@NonNull Context context, @NonNull EmojiEventListener emojiSelectionListener, @NonNull VariationSelectorListener variationSelectorListener, boolean allowVariations, - @NonNull RecyclerView.LayoutManager layoutManager, + @NonNull LinearLayoutManager layoutManager, @LayoutRes int displayItemLayoutResId) { super(context); - final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true); + initialize(emojiSelectionListener, variationSelectorListener, allowVariations, layoutManager, displayItemLayoutResId); + } + public void initialize(@NonNull EmojiEventListener emojiSelectionListener, + @NonNull VariationSelectorListener variationSelectorListener, + boolean allowVariations) + { + initialize(emojiSelectionListener, variationSelectorListener, allowVariations, new GridLayoutManager(getContext(), 8), R.layout.emoji_display_item); + Drawable drawable = DrawableUtil.tint(ContextUtil.requireDrawable(getContext(), R.drawable.triangle_bottom_right_corner), ContextCompat.getColor(getContext(), R.color.signal_button_secondary_text_disabled)); + addItemDecoration(new EmojiItemDecoration(allowVariations, drawable)); + } + + public void initialize(@NonNull EmojiEventListener emojiSelectionListener, + @NonNull VariationSelectorListener variationSelectorListener, + boolean allowVariations, + @NonNull LinearLayoutManager layoutManager, + @LayoutRes int displayItemLayoutResId) + { this.variationSelectorListener = variationSelectorListener; - this.recyclerView = view.findViewById(R.id.emoji); this.layoutManager = layoutManager; this.scrollDisabler = new ScrollDisabler(); - this.popup = new EmojiVariationSelectorPopup(context, emojiSelectionListener); + this.popup = new EmojiVariationSelectorPopup(getContext(), emojiSelectionListener); this.adapterFactory = () -> new EmojiPageViewGridAdapter(popup, emojiSelectionListener, this, allowVariations, displayItemLayoutResId); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setItemAnimator(null); + if (this.layoutManager instanceof GridLayoutManager) { + GridLayoutManager gridLayout = (GridLayoutManager) this.layoutManager; + gridLayout.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + if (getAdapter() != null) { + Optional> model = getAdapter().getModel(position); + if (model.isPresent() && (model.get() instanceof EmojiHeader || model.get() instanceof EmojiNoResultsModel)) { + return gridLayout.getSpanCount(); + } + } + return 1; + } + }); + } + + setLayoutManager(layoutManager); } public void presentForEmojiKeyboard() { - recyclerView.setPadding(recyclerView.getPaddingLeft(), - recyclerView.getPaddingTop(), - recyclerView.getPaddingRight(), - recyclerView.getPaddingBottom() + ViewUtil.dpToPx(56)); + setPadding(getPaddingLeft(), + getPaddingTop(), + getPaddingRight(), + getPaddingBottom() + ViewUtil.dpToPx(56)); - recyclerView.setClipToPadding(false); + setClipToPadding(false); } public void onSelected() { - if (model.isDynamic() && recyclerView.getAdapter() != null) { - recyclerView.getAdapter().notifyDataSetChanged(); + if (getAdapter() != null && (model == null || model.isDynamic())) { + getAdapter().notifyDataSetChanged(); } } + public void setList(@NonNull MappingModelList list) { + this.model = null; + EmojiPageViewGridAdapter adapter = adapterFactory.create(); + setAdapter(adapter); + adapter.submitList(list); + } + public void setModel(@Nullable EmojiPageModel model) { this.model = model; EmojiPageViewGridAdapter adapter = adapterFactory.create(); - recyclerView.setAdapter(adapter); + setAdapter(adapter); adapter.submitList(getMappingModelList()); } @@ -93,18 +145,21 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe this.model = model; EmojiPageViewGridAdapter adapter = adapterFactory.create(); - recyclerView.setAdapter(adapter); + setAdapter(adapter); adapter.submitList(getMappingModelList()); } private @NonNull MappingModelList getMappingModelList() { - MappingModelList mappingModels = new MappingModelList(); - if (model != null) { - mappingModels.addAll(Stream.of(model.getDisplayEmoji()).map(EmojiPageViewGridAdapter.EmojiModel::new).toList()); + boolean emoticonPage = EmojiCategory.EMOTICONS.getKey().equals(model.getKey()); + return model.getDisplayEmoji() + .stream() + .map(e -> emoticonPage ? new EmojiPageViewGridAdapter.EmojiTextModel(model.getKey(), e) + : new EmojiPageViewGridAdapter.EmojiModel(model.getKey(), e)) + .collect(MappingModelList.collect()); } - return mappingModels; + return new MappingModelList(); } @Override @@ -117,8 +172,8 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { if (layoutManager instanceof GridLayoutManager) { - int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width); - int spanCount = Math.max(w / idealWidth, 1); + int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width); + int spanCount = Math.max(w / idealWidth, 1); ((GridLayoutManager) layoutManager).setSpanCount(spanCount); } @@ -127,9 +182,9 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe @Override public void onVariationSelectorStateChanged(boolean open) { if (open) { - recyclerView.addOnItemTouchListener(scrollDisabler); + addOnItemTouchListener(scrollDisabler); } else { - post(() -> recyclerView.removeOnItemTouchListener(scrollDisabler)); + post(() -> removeOnItemTouchListener(scrollDisabler)); } if (variationSelectorListener != null) { @@ -138,7 +193,29 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe } public void setRecyclerNestedScrollingEnabled(boolean enabled) { - recyclerView.setNestedScrollingEnabled(enabled); + setNestedScrollingEnabled(enabled); + } + + public void smoothScrollToPositionTop(int position) { + int currentPosition = layoutManager.findFirstCompletelyVisibleItemPosition(); + boolean shortTrip = Math.abs(currentPosition - position) < 475; + + if (shortTrip) { + RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(getContext()) { + @Override + protected int getVerticalSnapPreference() { + return LinearSmoothScroller.SNAP_TO_START; + } + }; + smoothScroller.setTargetPosition(position); + layoutManager.startSmoothScroll(smoothScroller); + } else { + layoutManager.scrollToPositionWithOffset(position, 0); + } + } + + public @Nullable EmojiPageViewGridAdapter getAdapter() { + return (EmojiPageViewGridAdapter) super.getAdapter(); } private static class ScrollDisabler implements RecyclerView.OnItemTouchListener { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageViewGridAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageViewGridAdapter.java index 08041e292e..f622d4c21b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageViewGridAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageViewGridAdapter.java @@ -2,26 +2,22 @@ package org.thoughtcrime.securesms.components.emoji; import android.graphics.drawable.Drawable; import android.view.View; -import android.view.ViewGroup; import android.widget.ImageView; import android.widget.PopupWindow; -import android.widget.Space; +import android.widget.TextView; import androidx.annotation.LayoutRes; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import org.jetbrains.annotations.NotNull; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener; -import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView; import org.thoughtcrime.securesms.util.MappingAdapter; import org.thoughtcrime.securesms.util.MappingModel; import org.thoughtcrime.securesms.util.MappingViewHolder; public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWindow.OnDismissListener { - private final VariationSelectorListener variationSelectorListener; + private final VariationSelectorListener variationSelectorListener; public EmojiPageViewGridAdapter(@NonNull EmojiVariationSelectorPopup popup, @NonNull EmojiEventListener emojiEventListener, @@ -33,7 +29,10 @@ public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWin popup.setOnDismissListener(this); + registerFactory(EmojiHeader.class, new LayoutFactory<>(EmojiHeaderViewHolder::new, R.layout.emoji_grid_header)); registerFactory(EmojiModel.class, new LayoutFactory<>(v -> new EmojiViewHolder(v, emojiEventListener, variationSelectorListener, popup, allowVariations), displayItemLayoutResId)); + registerFactory(EmojiTextModel.class, new LayoutFactory<>(v -> new EmojiTextViewHolder(v, emojiEventListener), R.layout.emoji_text_display_item)); + registerFactory(EmojiNoResultsModel.class, new LayoutFactory<>(MappingViewHolder.SimpleViewHolder::new, R.layout.emoji_grid_no_results)); } @Override @@ -41,21 +40,73 @@ public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWin variationSelectorListener.onVariationSelectorStateChanged(false); } - static class EmojiModel implements MappingModel { + public static class EmojiHeader implements MappingModel, HasKey { - private final Emoji emoji; + private final String key; + private final int title; - EmojiModel(@NonNull Emoji emoji) { + public EmojiHeader(@NonNull String key, int title) { + this.key = key; + this.title = title; + } + + @Override + public @NonNull String getKey() { + return key; + } + + @Override + public boolean areItemsTheSame(@NonNull EmojiHeader newItem) { + return title == newItem.title; + } + + @Override + public boolean areContentsTheSame(@NonNull EmojiHeader newItem) { + return areItemsTheSame(newItem); + } + } + + static class EmojiHeaderViewHolder extends MappingViewHolder { + + private final TextView title; + + public EmojiHeaderViewHolder(@NonNull View itemView) { + super(itemView); + title = findViewById(R.id.emoji_grid_header_title); + } + + @Override + public void bind(@NonNull EmojiHeader model) { + title.setText(model.title); + } + } + + public static class EmojiModel implements MappingModel, HasKey { + + private final String key; + private final Emoji emoji; + + public EmojiModel(@NonNull String key, @NonNull Emoji emoji) { + this.key = key; this.emoji = emoji; } @Override - public boolean areItemsTheSame(@NonNull @NotNull EmojiModel newItem) { + public @NonNull String getKey() { + return key; + } + + public @NonNull Emoji getEmoji() { + return emoji; + } + + @Override + public boolean areItemsTheSame(@NonNull EmojiModel newItem) { return newItem.emoji.getValue().equals(emoji.getValue()); } @Override - public boolean areContentsTheSame(@NonNull @NotNull EmojiModel newItem) { + public boolean areContentsTheSame(@NonNull EmojiModel newItem) { return areItemsTheSame(newItem); } } @@ -67,9 +118,8 @@ public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWin private final EmojiEventListener emojiEventListener; private final boolean allowVariations; - private final ImageView imageView; - private final AsciiEmojiView textView; - private final ImageView hintCorner; + private final ImageView imageView; + private final ImageView hintCorner; public EmojiViewHolder(@NonNull View itemView, @NonNull EmojiEventListener emojiEventListener, @@ -85,31 +135,26 @@ public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWin this.allowVariations = allowVariations; this.imageView = itemView.findViewById(R.id.emoji_image); - this.textView = itemView.findViewById(R.id.emoji_text); this.hintCorner = itemView.findViewById(R.id.emoji_variation_hint); } @Override - public void bind(@NonNull @NotNull EmojiModel model) { + public void bind(@NonNull EmojiModel model) { final Drawable drawable = EmojiProvider.getEmojiDrawable(imageView.getContext(), model.emoji.getValue()); if (drawable != null) { - textView.setVisibility(View.GONE); imageView.setVisibility(View.VISIBLE); - imageView.setImageDrawable(drawable); - } else { - textView.setVisibility(View.VISIBLE); - imageView.setVisibility(View.GONE); - - textView.setEmoji(model.emoji.getValue()); } itemView.setOnClickListener(v -> { emojiEventListener.onEmojiSelected(model.emoji.getValue()); }); - if (allowVariations && model.emoji.getVariations().size() > 1) { + if (allowVariations && model.emoji.hasMultipleVariations()) { + if (hintCorner != null) { + hintCorner.setVisibility(View.VISIBLE); + } itemView.setOnLongClickListener(v -> { popup.dismiss(); popup.setVariations(model.emoji.getVariations()); @@ -117,14 +162,84 @@ public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWin variationSelectorListener.onVariationSelectorStateChanged(true); return true; }); - hintCorner.setVisibility(View.VISIBLE); } else { + if (hintCorner != null) { + hintCorner.setVisibility(View.GONE); + } itemView.setOnLongClickListener(null); - hintCorner.setVisibility(View.GONE); } } } + public static class EmojiTextModel implements MappingModel, HasKey { + private final String key; + private final Emoji emoji; + + public EmojiTextModel(@NonNull String key, @NonNull Emoji emoji) { + this.key = key; + this.emoji = emoji; + } + + @Override + public @NonNull String getKey() { + return key; + } + + public @NonNull Emoji getEmoji() { + return emoji; + } + + @Override + public boolean areItemsTheSame(@NonNull EmojiTextModel newItem) { + return newItem.emoji.getValue().equals(emoji.getValue()); + } + + @Override + public boolean areContentsTheSame(@NonNull EmojiTextModel newItem) { + return areItemsTheSame(newItem); + } + } + + static class EmojiTextViewHolder extends MappingViewHolder { + + private final EmojiEventListener emojiEventListener; + private final AsciiEmojiView textView; + + public EmojiTextViewHolder(@NonNull View itemView, + @NonNull EmojiEventListener emojiEventListener) + { + super(itemView); + + this.emojiEventListener = emojiEventListener; + this.textView = itemView.findViewById(R.id.emoji_text); + } + + @Override + public void bind(@NonNull EmojiTextModel model) { + textView.setEmoji(model.emoji.getValue()); + + itemView.setOnClickListener(v -> { + emojiEventListener.onEmojiSelected(model.emoji.getValue()); + }); + } + } + + public static class EmojiNoResultsModel implements MappingModel { + @Override + public boolean areItemsTheSame(@NonNull EmojiNoResultsModel newItem) { + return true; + } + + @Override + public boolean areContentsTheSame(@NonNull EmojiNoResultsModel newItem) { + return true; + } + } + + public interface HasKey { + @NonNull String getKey(); + } + public interface VariationSelectorListener { void onVariationSelectorStateChanged(boolean open); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java index e43a5d3c2e..cf5110e26e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java @@ -28,6 +28,7 @@ import java.util.List; public class RecentEmojiPageModel implements EmojiPageModel { private static final String TAG = Log.tag(RecentEmojiPageModel.class); private static final int EMOJI_LRU_SIZE = 50; + public static final String KEY = "Recents"; private final SharedPreferences prefs; private final String preferenceName; @@ -55,6 +56,11 @@ public class RecentEmojiPageModel implements EmojiPageModel { } } + @Override + public String getKey() { + return KEY; + } + @Override public int getIconAttr() { return R.attr.emoji_category_recent; } @@ -100,13 +106,4 @@ public class RecentEmojiPageModel implements EmojiPageModel { } }); } - - private String[] toReversePrimitiveArray(@NonNull LinkedHashSet emojiSet) { - String[] emojis = new String[emojiSet.size()]; - int i = emojiSet.size() - 1; - for (String emoji : emojiSet) { - emojis[i--] = emoji; - } - return emojis; - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java index 41fe4d9aa6..2433437f23 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java @@ -2,39 +2,39 @@ package org.thoughtcrime.securesms.components.emoji; import android.net.Uri; -import androidx.annotation.AttrRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import java.util.ArrayList; +import org.thoughtcrime.securesms.emoji.EmojiCategory; + +import java.util.Arrays; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; public class StaticEmojiPageModel implements EmojiPageModel { - @AttrRes private final int iconAttr; - @NonNull private final List emoji; - @Nullable private final Uri sprite; + private final @NonNull EmojiCategory category; + private final @NonNull List emoji; + private final @Nullable Uri sprite; - public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull String[] strings, @Nullable Uri sprite) { - List emoji = new ArrayList<>(strings.length); - for (String s : strings) { - emoji.add(new Emoji(Collections.singletonList(s))); - } + public StaticEmojiPageModel(@NonNull EmojiCategory category, @NonNull String[] strings, @Nullable Uri sprite) { + this(category, Arrays.stream(strings).map(s -> new Emoji(Collections.singletonList(s))).collect(Collectors.toList()), sprite); + } - this.iconAttr = iconAttr; - this.emoji = emoji; + public StaticEmojiPageModel(@NonNull EmojiCategory category, @NonNull List emoji, @Nullable Uri sprite) { + this.category = category; + this.emoji = Collections.unmodifiableList(emoji); this.sprite = sprite; } - public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull List emoji, @Nullable Uri sprite) { - this.iconAttr = iconAttr; - this.emoji = Collections.unmodifiableList(emoji); - this.sprite = sprite; + @Override + public String getKey() { + return category.getKey(); } public int getIconAttr() { - return iconAttr; + return category.getIcon(); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 20b4714147..2557d2e300 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -2337,7 +2337,7 @@ public class ConversationActivity extends PassphraseRequiredActivity messageRecord.isMms(), oldRecord)); } else { - reactionDelegate.hideAllButMask(); + reactionDelegate.hideForReactWithAny(); ReactWithAnyEmojiBottomSheetDialogFragment.createForMessageRecord(messageRecord, reactWithAnyEmojiStartPage) .show(getSupportFragmentManager(), "BOTTOM"); @@ -2349,11 +2349,6 @@ public class ConversationActivity extends PassphraseRequiredActivity reactionDelegate.hideMask(); } - @Override - public void onReactWithAnyEmojiPageChanged(int page) { - reactWithAnyEmojiStartPage = page; - } - @Override public void onReactWithAnyEmojiSelected(@NonNull String emoji) { } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionDelegate.java index bd883f3ff9..92506bc458 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionDelegate.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionDelegate.java @@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation; import android.app.Activity; import android.graphics.PointF; import android.view.MotionEvent; -import android.view.View; import androidx.annotation.NonNull; import androidx.appcompat.widget.Toolbar; @@ -55,8 +54,8 @@ final class ConversationReactionDelegate { overlayStub.get().hide(); } - void hideAllButMask() { - overlayStub.get().hideAllButMask(); + void hideForReactWithAny() { + overlayStub.get().hideForReactWithAny(); } void hideMask() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java index 2a739231b4..1b85902527 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationReactionOverlay.java @@ -16,7 +16,6 @@ import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.RelativeLayout; -import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; @@ -228,8 +227,8 @@ public final class ConversationReactionOverlay extends RelativeLayout { hideInternal(hideAnimatorSet, onHideListener); } - public void hideAllButMask() { - hideInternal(hideAllButMaskAnimatorSet, null); + public void hideForReactWithAny() { + hideInternal(hideAnimatorSet, null); } public void hideMask() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiCategory.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiCategory.kt index 15800993a5..4dfc2c2bc4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiCategory.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiCategory.kt @@ -18,6 +18,7 @@ enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon: EMOTICONS(8, "Emoticons", R.attr.emoji_category_emoticons); companion object { + @JvmStatic fun forKey(key: String) = values().first { it.key == key } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiJsonParser.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiJsonParser.kt index 44fab16200..d54dc5e743 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiJsonParser.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiJsonParser.kt @@ -74,7 +74,7 @@ object EmojiJsonParser { } } - return StaticEmojiPageModel(category.icon, pageList, uriFactory(pageName, format)) + return StaticEmojiPageModel(category, pageList, uriFactory(pageName, format)) } private fun mergeToDisplayPages(dataPages: List): List { diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt index c9d41285a8..7f47d80ba9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt @@ -152,7 +152,7 @@ data class EmojiMetrics(val rawHeight: Int, val rawWidth: Int, val perRow: Int) private fun getAssetsUri(name: String, format: String): Uri = Uri.parse("file:///android_asset/emoji/$name.$format") private val PAGE_EMOTICONS: EmojiPageModel = StaticEmojiPageModel( - EmojiCategory.EMOTICONS.icon, + EmojiCategory.EMOTICONS, arrayOf( ":-)", ";-)", "(-:", ":->", ":-D", "\\o/", ":-P", "B-)", ":-$", ":-*", "O:-)", "=-O", diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java index 5b2ffbcba2..85aa765083 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java @@ -59,7 +59,7 @@ public class GiphyActivity extends PassphraseRequiredActivity implements Keyboar private void initializeToolbar() { KeyboardPageSearchView searchView = findViewById(R.id.giphy_search_text); searchView.setCallbacks(this); - searchView.enableBackNavigation(); + searchView.enableBackNavigation(true); ViewUtil.focusAndShowKeyboard(searchView); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPageCategoryIconViewHolder.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPageCategoryIconViewHolder.kt index fa559012f3..42678b4301 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPageCategoryIconViewHolder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPageCategoryIconViewHolder.kt @@ -7,6 +7,7 @@ import androidx.appcompat.widget.AppCompatImageView import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.util.MappingModel import org.thoughtcrime.securesms.util.MappingViewHolder +import java.util.function.Consumer interface KeyboardPageCategoryIconMappingModel> : MappingModel { val key: String @@ -15,14 +16,14 @@ interface KeyboardPageCategoryIconMappingModel>(itemView: View, private val onPageSelected: (String) -> Unit) : MappingViewHolder(itemView) { +class KeyboardPageCategoryIconViewHolder>(itemView: View, private val onPageSelected: Consumer) : MappingViewHolder(itemView) { private val iconView: AppCompatImageView = itemView.findViewById(R.id.category_icon) private val iconSelected: View = itemView.findViewById(R.id.category_icon_selected) override fun bind(model: T) { itemView.setOnClickListener { - onPageSelected(model.key) + onPageSelected.accept(model.key) } iconView.setImageDrawable(model.getIcon(context)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoriesAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoriesAdapter.kt index 3d0f23918d..f45cd24dad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoriesAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoriesAdapter.kt @@ -3,8 +3,9 @@ package org.thoughtcrime.securesms.keyboard.emoji import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.keyboard.KeyboardPageCategoryIconViewHolder import org.thoughtcrime.securesms.util.MappingAdapter +import java.util.function.Consumer -class EmojiKeyboardPageCategoriesAdapter(private val onPageSelected: (String) -> Unit) : MappingAdapter() { +class EmojiKeyboardPageCategoriesAdapter(private val onPageSelected: Consumer) : MappingAdapter() { init { registerFactory(EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel::class.java, LayoutFactory({ v -> KeyboardPageCategoryIconViewHolder(v, onPageSelected) }, R.layout.keyboard_pager_category_icon)) registerFactory(EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel::class.java, LayoutFactory({ v -> KeyboardPageCategoryIconViewHolder(v, onPageSelected) }, R.layout.keyboard_pager_category_icon)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoryMappingModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoryMappingModel.kt index 5b7c105ffa..f9b7b39537 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoryMappingModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoryMappingModel.kt @@ -4,6 +4,7 @@ import android.content.Context import android.graphics.drawable.Drawable import androidx.annotation.AttrRes import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel import org.thoughtcrime.securesms.emoji.EmojiCategory import org.thoughtcrime.securesms.keyboard.KeyboardPageCategoryIconMappingModel import org.thoughtcrime.securesms.util.ThemeUtil @@ -22,14 +23,10 @@ sealed class EmojiKeyboardPageCategoryMappingModel( return newItem.key == key } - class RecentsMappingModel(selected: Boolean) : EmojiKeyboardPageCategoryMappingModel(KEY, R.attr.emoji_category_recent, selected) { + class RecentsMappingModel(selected: Boolean) : EmojiKeyboardPageCategoryMappingModel(RecentEmojiPageModel.KEY, R.attr.emoji_category_recent, selected) { override fun areContentsTheSame(newItem: EmojiKeyboardPageCategoryMappingModel): Boolean { return newItem is RecentsMappingModel && super.areContentsTheSame(newItem) } - - companion object { - const val KEY = "Recents" - } } class EmojiCategoryMappingModel(private val emojiCategory: EmojiCategory, selected: Boolean) : EmojiKeyboardPageCategoryMappingModel(emojiCategory.key, emojiCategory.icon, selected) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageViewModel.kt index 26db7dcb10..7b0ba5994d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageViewModel.kt @@ -20,7 +20,7 @@ class EmojiKeyboardPageViewModel : ViewModel() { val categories: LiveData = Transformations.map(internalSelectedKey) { selected -> MappingModelList().apply { - add(EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel(selected == EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel.KEY)) + add(EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel(selected == RecentEmojiPageModel.KEY)) EmojiCategory.values().forEach { add(EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel(it, it.key == selected)) @@ -41,7 +41,7 @@ class EmojiKeyboardPageViewModel : ViewModel() { } private fun getPageForCategory(mappingModel: EmojiKeyboardPageCategoryMappingModel): EmojiPageMappingModel { - val page = if (mappingModel.key == EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel.KEY) { + val page = if (mappingModel.key == RecentEmojiPageModel.KEY) { RecentEmojiPageModel(ApplicationDependencies.getApplication(), EmojiKeyboardProvider.RECENT_STORAGE_KEY) } else { EmojiSource.latest.displayPages.first { it.iconAttr == mappingModel.iconId } @@ -57,7 +57,7 @@ class EmojiKeyboardPageViewModel : ViewModel() { companion object { fun getStartingTab(): String { return if (RecentEmojiPageModel.hasRecents(ApplicationDependencies.getApplication(), EmojiKeyboardProvider.RECENT_STORAGE_KEY)) { - EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel.KEY + RecentEmojiPageModel.KEY } else { EmojiCategory.PEOPLE.key } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt index df3308685e..7c39017af5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt @@ -71,9 +71,7 @@ class KeyboardPageSearchView @JvmOverloads constructor( } } - clearButton.setOnClickListener { - input.text.clear() - } + clearButton.setOnClickListener { clearQuery() } context.obtainStyledAttributes(attrs, R.styleable.KeyboardPageSearchView, 0, 0).use { typedArray -> val showAlways: Boolean = typedArray.getBoolean(R.styleable.KeyboardPageSearchView_show_always, false) @@ -97,6 +95,7 @@ class KeyboardPageSearchView @JvmOverloads constructor( val iconTint = typedArray.getColorStateList(R.styleable.KeyboardPageSearchView_search_icon_tint) ?: ContextCompat.getColorStateList(context, R.color.signal_icon_tint_primary) ImageViewCompat.setImageTintList(navButton, iconTint) ImageViewCompat.setImageTintList(clearButton, iconTint) + input.setHintTextColor(iconTint) val clickOnly: Boolean = typedArray.getBoolean(R.styleable.KeyboardPageSearchView_click_only, false) if (clickOnly) { @@ -109,10 +108,14 @@ class KeyboardPageSearchView @JvmOverloads constructor( fun showRequested(): Boolean = state == State.SHOW_REQUESTED - fun enableBackNavigation() { - navButton.setImageResource(R.drawable.ic_arrow_left_24) - navButton.setOnClickListener { - callbacks?.onNavigationClicked() + fun enableBackNavigation(enable: Boolean = true) { + navButton.setImageResource(if (enable) R.drawable.ic_arrow_left_24 else R.drawable.ic_search_24) + if (enable) { + navButton.setImageResource(R.drawable.ic_arrow_left_24) + navButton.setOnClickListener { callbacks?.onNavigationClicked() } + } else { + navButton.setImageResource(R.drawable.ic_search_24) + navButton.setOnClickListener(null) } } @@ -168,6 +171,15 @@ class KeyboardPageSearchView @JvmOverloads constructor( enableBackNavigation() } + override fun clearFocus() { + super.clearFocus() + clearChildFocus(input) + } + + fun clearQuery() { + input.text.clear() + } + interface Callbacks { fun onFocusLost() = Unit fun onFocusGained() = Unit diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchRepository.kt index a8d59c5ae1..79c06e3967 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchRepository.kt @@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.EmojiSearchDatabase import org.thoughtcrime.securesms.emoji.EmojiSource +import java.util.function.Consumer private const val MINIMUM_QUERY_THRESHOLD = 1 private const val EMOJI_SEARCH_LIMIT = 20 @@ -18,18 +19,18 @@ class EmojiSearchRepository(private val context: Context) { private val emojiSearchDatabase: EmojiSearchDatabase = DatabaseFactory.getEmojiSearchDatabase(context) - fun submitQuery(query: String, consumer: (EmojiPageModel) -> Unit) { - if (query.length < MINIMUM_QUERY_THRESHOLD) { - consumer(RecentEmojiPageModel(context, EmojiKeyboardProvider.RECENT_STORAGE_KEY)) + fun submitQuery(query: String, includeRecents: Boolean, limit: Int = EMOJI_SEARCH_LIMIT, consumer: Consumer) { + if (query.length < MINIMUM_QUERY_THRESHOLD && includeRecents) { + consumer.accept(RecentEmojiPageModel(context, EmojiKeyboardProvider.RECENT_STORAGE_KEY)) } else { SignalExecutors.SERIAL.execute { - val emoji: List = emojiSearchDatabase.query(query, EMOJI_SEARCH_LIMIT) + val emoji: List = emojiSearchDatabase.query(query, limit) val displayEmoji: List = emoji .mapNotNull { canonical -> EmojiSource.latest.canonicalToVariations[canonical] } .map { Emoji(it) } - consumer(EmojiSearchResultsPageModel(emoji, displayEmoji)) + consumer.accept(EmojiSearchResultsPageModel(emoji, displayEmoji)) } } } @@ -38,6 +39,8 @@ class EmojiSearchRepository(private val context: Context) { private val emoji: List, private val displayEmoji: List ) : EmojiPageModel { + override fun getKey(): String = "" + override fun getIconAttr(): Int = -1 override fun getEmoji(): List = emoji diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchViewModel.kt index ff39708ca9..e987a12246 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchViewModel.kt @@ -17,7 +17,7 @@ class EmojiSearchViewModel(private val repository: EmojiSearchRepository) : View } fun onQueryChanged(query: String) { - repository.submitQuery(query, internalPageModel::postValue) + repository.submitQuery(query = query, includeRecents = true, consumer = internalPageModel::postValue) } class Factory(private val repository: EmojiSearchRepository) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileActivity.java b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileActivity.java index 1a154b8008..12139b4348 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/profiles/manage/ManageProfileActivity.java @@ -65,10 +65,6 @@ public class ManageProfileActivity extends PassphraseRequiredActivity implements public void onReactWithAnyEmojiDialogDismissed() { } - @Override - public void onReactWithAnyEmojiPageChanged(int page) { - } - @Override public void onReactWithAnyEmojiSelected(@NonNull String emoji) { NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().getPrimaryNavigationFragment(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiAdapter.java deleted file mode 100644 index 2343f98235..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiAdapter.java +++ /dev/null @@ -1,181 +0,0 @@ -package org.thoughtcrime.securesms.reactions.any; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.core.widget.NestedScrollView; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.ListAdapter; -import androidx.recyclerview.widget.RecyclerView; - -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; - -final class ReactWithAnyEmojiAdapter extends ListAdapter { - - private static final int VIEW_TYPE_SINGLE = 0; - private static final int VIEW_TYPE_DUAL = 1; - - private final EmojiKeyboardProvider.EmojiEventListener emojiEventListener; - private final EmojiPageViewGridAdapter.VariationSelectorListener variationSelectorListener; - private final Callbacks callbacks; - - ReactWithAnyEmojiAdapter(@NonNull EmojiKeyboardProvider.EmojiEventListener emojiEventListener, - @NonNull EmojiPageViewGridAdapter.VariationSelectorListener variationSelectorListener, - @NonNull Callbacks callbacks) - { - super(new PageChangedCallback()); - - this.emojiEventListener = emojiEventListener; - this.variationSelectorListener = variationSelectorListener; - this.callbacks = callbacks; - } - - public ReactWithAnyEmojiPage getItem(int position) { - return super.getItem(position); - } - - @Override - public @NonNull ReactWithAnyEmojiPageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - switch (viewType) { - case VIEW_TYPE_SINGLE: - return new SinglePageBlockViewHolder(createEmojiPageView(parent.getContext())); - case VIEW_TYPE_DUAL: - EmojiPageView block1 = createEmojiPageView(parent.getContext()); - EmojiPageView block2 = createEmojiPageView(parent.getContext()); - NestedScrollView scrollView = (NestedScrollView) LayoutInflater.from(parent.getContext()).inflate(R.layout.react_with_any_emoji_dual_block_item, parent, false); - LinearLayout container = scrollView.findViewById(R.id.react_with_any_emoji_dual_block_item_container); - - block1.setRecyclerNestedScrollingEnabled(false); - block2.setRecyclerNestedScrollingEnabled(false); - - container.addView(block1, 0); - container.addView(block2); - - return new DualPageBlockViewHolder(scrollView, block1, block2); - default: - throw new IllegalArgumentException("Unknown viewType: " + viewType); - } - } - - @Override - public void onBindViewHolder(@NonNull ReactWithAnyEmojiPageViewHolder holder, int position) { - holder.bind(getItem(position)); - } - - @Override - public void onViewAttachedToWindow(@NonNull ReactWithAnyEmojiPageViewHolder holder) { - callbacks.onViewHolderAttached(holder.getAdapterPosition(), holder); - } - - @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); - } - - @Override - public int getItemViewType(int position) { - return getItem(position).getPageBlocks().size() > 1 ? VIEW_TYPE_DUAL : VIEW_TYPE_SINGLE; - } - - private EmojiPageView createEmojiPageView(@NonNull Context context) { - return new EmojiPageView(context, emojiEventListener, variationSelectorListener, true); - } - - static abstract class ReactWithAnyEmojiPageViewHolder extends RecyclerView.ViewHolder implements ScrollableChild { - - public ReactWithAnyEmojiPageViewHolder(@NonNull View itemView) { - super(itemView); - } - - abstract void bind(@NonNull ReactWithAnyEmojiPage reactWithAnyEmojiPage); - } - - static final class SinglePageBlockViewHolder extends ReactWithAnyEmojiPageViewHolder { - - private final EmojiPageView emojiPageView; - - public SinglePageBlockViewHolder(@NonNull View itemView) { - super(itemView); - - emojiPageView = (EmojiPageView) itemView; - - ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); - - emojiPageView.setLayoutParams(params); - } - - @Override - void bind(@NonNull ReactWithAnyEmojiPage reactWithAnyEmojiPage) { - emojiPageView.setModel(reactWithAnyEmojiPage.getPageBlocks().get(0).getPageModel()); - } - - @Override - public void setNestedScrollingEnabled(boolean isEnabled) { - emojiPageView.setRecyclerNestedScrollingEnabled(isEnabled); - } - } - - static final class DualPageBlockViewHolder extends ReactWithAnyEmojiPageViewHolder { - - private final EmojiPageView block1; - private final EmojiPageView block2; - private final TextView block2Label; - - public DualPageBlockViewHolder(@NonNull View itemView, - @NonNull EmojiPageView block1, - @NonNull EmojiPageView block2) - { - super(itemView); - - this.block1 = block1; - this.block2 = block2; - this.block2Label = itemView.findViewById(R.id.react_with_any_emoji_dual_block_item_block_2_label); - } - - @Override - void bind(@NonNull ReactWithAnyEmojiPage reactWithAnyEmojiPage) { - block1.setModel(reactWithAnyEmojiPage.getPageBlocks().get(0).getPageModel()); - block2.setModel(reactWithAnyEmojiPage.getPageBlocks().get(1).getPageModel()); - block2Label.setText(reactWithAnyEmojiPage.getPageBlocks().get(1).getLabel()); - } - - @Override - public void setNestedScrollingEnabled(boolean isEnabled) { - ((NestedScrollView) itemView).setNestedScrollingEnabled(isEnabled); - } - } - - interface Callbacks { - void onViewHolderAttached(int adapterPosition, ScrollableChild pageView); - } - - interface ScrollableChild { - void setNestedScrollingEnabled(boolean isEnabled); - } - - private static class PageChangedCallback extends DiffUtil.ItemCallback { - - @Override - public boolean areItemsTheSame(@NonNull ReactWithAnyEmojiPage oldItem, @NonNull ReactWithAnyEmojiPage newItem) { - return oldItem.getLabel() == newItem.getLabel(); - } - - @Override - public boolean areContentsTheSame(@NonNull ReactWithAnyEmojiPage oldItem, @NonNull ReactWithAnyEmojiPage newItem) { - return oldItem.equals(newItem); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java index 0056226426..2c7c510898 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java @@ -5,13 +5,12 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; -import android.util.SparseArray; +import android.text.TextUtils; 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.NonNull; import androidx.annotation.Nullable; @@ -20,7 +19,8 @@ import androidx.core.view.ViewCompat; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.ViewModelProviders; import androidx.loader.app.LoaderManager; -import androidx.viewpager2.widget.ViewPager2; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetDialog; @@ -28,21 +28,26 @@ 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.keyboard.emoji.EmojiKeyboardPageCategoriesAdapter; +import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoryMappingModel; +import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView; import org.thoughtcrime.securesms.reactions.ReactionsLoader; import org.thoughtcrime.securesms.reactions.edit.EditReactionsActivity; -import org.thoughtcrime.securesms.util.ThemeUtil; +import org.thoughtcrime.securesms.util.MappingModel; import org.thoughtcrime.securesms.util.ViewUtil; -public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomSheetDialogFragment - implements EmojiKeyboardProvider.EmojiEventListener, - EmojiPageViewGridAdapter.VariationSelectorListener +import java.util.Optional; + +import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; + +public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomSheetDialogFragment implements EmojiKeyboardProvider.EmojiEventListener, + EmojiPageViewGridAdapter.VariationSelectorListener { private static final String REACTION_STORAGE_KEY = "reactions_recent_emoji"; @@ -51,20 +56,17 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee private static final String ARG_MESSAGE_ID = "arg_message_id"; private static final String ARG_IS_MMS = "arg_is_mms"; private static final String ARG_START_PAGE = "arg_start_page"; - private static final String ARG_SHADOWS = "arg_shadows"; private static final String ARG_RECENT_KEY = "arg_recent_key"; private static final String ARG_EDIT = "arg_edit"; - private ReactWithAnyEmojiViewModel viewModel; - private TextSwitcher categoryLabel; - private ViewPager2 categoryPager; - private ReactWithAnyEmojiAdapter adapter; - private OnPageChanged onPageChanged; - private SparseArray pageArray = new SparseArray<>(); - private Callback callback; - private ReactionsLoader reactionsLoader; - private View customizeReactions; - private boolean showEditReactions; + private ReactWithAnyEmojiViewModel viewModel; + private Callback callback; + private ReactionsLoader reactionsLoader; + private EmojiPageView emojiPageView; + private KeyboardPageSearchView search; + private View tabBar; + + private final UpdateCategorySelectionOnScroll categoryUpdateOnScroll = new UpdateCategorySelectionOnScroll(); public static DialogFragment createForMessageRecord(@NonNull MessageRecord messageRecord, int startingPage) { DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment(); @@ -73,7 +75,6 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee args.putLong(ARG_MESSAGE_ID, messageRecord.getId()); args.putBoolean(ARG_IS_MMS, messageRecord.isMms()); args.putInt(ARG_START_PAGE, startingPage); - args.putBoolean(ARG_SHADOWS, false); args.putString(ARG_RECENT_KEY, REACTION_STORAGE_KEY); args.putBoolean(ARG_EDIT, true); fragment.setArguments(args); @@ -88,7 +89,6 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee args.putLong(ARG_MESSAGE_ID, -1); args.putBoolean(ARG_IS_MMS, false); args.putInt(ARG_START_PAGE, -1); - args.putBoolean(ARG_SHADOWS, true); args.putString(ARG_RECENT_KEY, ABOUT_STORAGE_KEY); fragment.setArguments(args); @@ -102,7 +102,6 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee args.putLong(ARG_MESSAGE_ID, -1); args.putBoolean(ARG_IS_MMS, false); args.putInt(ARG_START_PAGE, -1); - args.putBoolean(ARG_SHADOWS, false); args.putString(ARG_RECENT_KEY, REACTION_STORAGE_KEY); fragment.setArguments(args); @@ -122,50 +121,41 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee @Override public void onCreate(@Nullable Bundle savedInstanceState) { - boolean shadows = requireArguments().getBoolean(ARG_SHADOWS); - if (ThemeUtil.isDarkTheme(requireContext())) { - setStyle(DialogFragment.STYLE_NORMAL, shadows ? R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny - : R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny_Shadowless); - } else { - setStyle(DialogFragment.STYLE_NORMAL, shadows ? R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny - : R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny_Shadowless); - } - super.onCreate(savedInstanceState); + setStyle(DialogFragment.STYLE_NORMAL, R.style.Widget_Signal_ReactWithAny); } @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); + BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState); + dialog.getBehavior().setPeekHeight((int) (getResources().getDisplayMetrics().heightPixels * 0.50)); - dialogBackground.setTint(ContextCompat.getColor(requireContext(), R.color.signal_background_dialog)); + ShapeAppearanceModel shapeAppearanceModel = ShapeAppearanceModel.builder() + .setTopLeftCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 18)) + .setTopRightCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 18)) + .build(); + + MaterialShapeDrawable dialogBackground = new MaterialShapeDrawable(shapeAppearanceModel); + + dialogBackground.setTint(ContextCompat.getColor(requireContext(), R.color.react_with_any_background)); dialog.getBehavior().addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { @Override public void onStateChanged(@NonNull View bottomSheet, int newState) { - if (newState == BottomSheetBehavior.STATE_EXPANDED) { + if (bottomSheet.getBackground() != dialogBackground) { ViewCompat.setBackground(bottomSheet, dialogBackground); } } @Override - public void onSlide(@NonNull View bottomSheet, float slideOffset) { - } + public void onSlide(@NonNull View bottomSheet, float slideOffset) { } }); return dialog; } @Override - public @Nullable View onCreateView(@NonNull LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) - { + 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); } @@ -177,36 +167,14 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee LoaderManager.getInstance(requireActivity()).initLoader((int) requireArguments().getLong(ARG_MESSAGE_ID), null, reactionsLoader); + emojiPageView = view.findViewById(R.id.react_with_any_emoji_page_view); + emojiPageView.initialize(this, this, true); + emojiPageView.addOnScrollListener(categoryUpdateOnScroll); + + search = view.findViewById(R.id.react_with_any_emoji_search); + search.setCallbacks(new SearchCallbacks()); + initializeViewModel(); - - categoryLabel = view.findViewById(R.id.category_label); - categoryPager = view.findViewById(R.id.category_pager); - - showEditReactions = requireArguments().getBoolean(ARG_EDIT, false); - - adapter = new ReactWithAnyEmojiAdapter(this, this, (position, pageView) -> { - pageArray.put(position, pageView); - - if (categoryPager.getCurrentItem() == position) { - updateFocusedRecycler(position); - } - }); - - onPageChanged = new OnPageChanged(); - - categoryPager.setAdapter(adapter); - categoryPager.registerOnPageChangeCallback(onPageChanged); - - viewModel.getEmojiPageModels().observe(getViewLifecycleOwner(), pages -> { - int pageToSet = adapter.getItemCount() == 0 ? getStartingPage((pages.get(0).hasEmoji())) - : -1; - - adapter.submitList(pages); - - if (pageToSet >= 0) { - categoryPager.setCurrentItem(pageToSet); - } - }); } @Override @@ -214,32 +182,51 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee super.onActivityCreated(savedInstanceState); if (savedInstanceState == null) { - FrameLayout container = requireDialog().findViewById(R.id.container); - LayoutInflater layoutInflater = LayoutInflater.from(requireContext()); - View tabBar = layoutInflater.inflate(R.layout.react_with_any_emoji_tabs, container, false); - TabLayout categoryTabs = tabBar.findViewById(R.id.category_tabs); + EmojiKeyboardPageCategoriesAdapter categoriesAdapter = new EmojiKeyboardPageCategoriesAdapter(key -> { + scrollTo(key); + viewModel.selectPage(key); + }); - customizeReactions = tabBar.findViewById(R.id.customize_reactions_frame); - if (showEditReactions) { + FrameLayout container = requireDialog().findViewById(R.id.container); + tabBar = LayoutInflater.from(requireContext()) + .inflate(R.layout.react_with_any_emoji_tabs, + container, + false); + RecyclerView categoriesRecycler = tabBar.findViewById(R.id.emoji_categories_recycler); + categoriesRecycler.setAdapter(categoriesAdapter); + + if (requireArguments().getBoolean(ARG_EDIT, false)) { + View customizeReactions = tabBar.findViewById(R.id.customize_reactions_frame); customizeReactions.setVisibility(View.VISIBLE); - tabBar.findViewById(R.id.customize_reactions).setOnClickListener(v -> startActivity(new Intent(requireContext(), EditReactionsActivity.class))); - } - - if (!requireArguments().getBoolean(ARG_SHADOWS)) { - View statusBarShader = layoutInflater.inflate(R.layout.react_with_any_emoji_status_fade, container, false); - ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtil.getStatusBarHeight(container)); - - statusBarShader.setLayoutParams(params); - container.addView(statusBarShader, 0); + customizeReactions.setOnClickListener(v -> startActivity(new Intent(requireContext(), EditReactionsActivity.class))); } container.addView(tabBar); - ViewCompat.setOnApplyWindowInsetsListener(container, (v, insets) -> insets.consumeSystemWindowInsets()); - new TabLayoutMediator(categoryTabs, categoryPager, (tab, position) -> { - tab.setCustomView(R.layout.react_with_any_emoji_tab) - .setIcon(ThemeUtil.getThemedDrawable(requireContext(), adapter.getItem(position).getIconAttr())); - }).attach(); + emojiPageView.addOnScrollListener(new TopAndBottomShadowHelper(requireView().findViewById(R.id.react_with_any_emoji_top_shadow), + tabBar.findViewById(R.id.react_with_any_emoji_bottom_shadow))); + + viewModel.getEmojiList().observe(getViewLifecycleOwner(), pages -> emojiPageView.setList(pages)); + viewModel.getCategories().observe(getViewLifecycleOwner(), categoriesAdapter::submitList); + viewModel.getSelectedKey().observe(getViewLifecycleOwner(), key -> categoriesRecycler.post(() -> { + int index = categoriesAdapter.indexOfFirst(EmojiKeyboardPageCategoryMappingModel.class, m -> m.getKey().equals(key)); + + if (index != -1) { + categoriesRecycler.smoothScrollToPosition(index); + } + })); + } + } + + private void scrollTo(@NonNull String key) { + if (emojiPageView.getAdapter() != null) { + int index = emojiPageView.getAdapter().indexOfFirst(EmojiPageViewGridAdapter.EmojiHeader.class, m -> m.getKey().equals(key)); + + if (index != -1) { + ((BottomSheetDialog) requireDialog()).getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED); + categoryUpdateOnScroll.startAutoScrolling(); + emojiPageView.smoothScrollToPositionTop(index); + } } } @@ -247,8 +234,6 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee public void onDestroyView() { super.onDestroyView(); LoaderManager.getInstance(requireActivity()).destroyLoader((int) requireArguments().getLong(ARG_MESSAGE_ID)); - - categoryPager.unregisterOnPageChangeCallback(onPageChanged); } @Override @@ -260,7 +245,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee private void initializeViewModel() { Bundle args = requireArguments(); - ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext(), args.getString(ARG_RECENT_KEY)); + ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext(), args.getString(ARG_RECENT_KEY, REACTION_STORAGE_KEY)); ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(reactionsLoader, repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS)); viewModel = ViewModelProviders.of(this, factory).get(ReactWithAnyEmojiViewModel.class); @@ -278,40 +263,72 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee } @Override - public void onVariationSelectorStateChanged(boolean open) { - categoryPager.setUserInputEnabled(!open); - } - - private void updateFocusedRecycler(int position) { - for (int i = 0; i < pageArray.size(); i++) { - pageArray.valueAt(i).setNestedScrollingEnabled(false); - } - - ReactWithAnyEmojiAdapter.ScrollableChild toFocus = pageArray.get(position); - if (toFocus != null) { - toFocus.setNestedScrollingEnabled(true); - categoryPager.requestLayout(); - } - - categoryLabel.setText(getString(adapter.getItem(position).getLabel())); - } - - private int getStartingPage(boolean firstPageHasContent) { - int startPage = requireArguments().getInt(ARG_START_PAGE); - return startPage >= 0 ? startPage : (firstPageHasContent ? 0 : 1); - } - - private class OnPageChanged extends ViewPager2.OnPageChangeCallback { - @Override - public void onPageSelected(int position) { - updateFocusedRecycler(position); - callback.onReactWithAnyEmojiPageChanged(position); - } - } + public void onVariationSelectorStateChanged(boolean open) { } public interface Callback { void onReactWithAnyEmojiDialogDismissed(); - void onReactWithAnyEmojiPageChanged(int page); + void onReactWithAnyEmojiSelected(@NonNull String emoji); } + + private class UpdateCategorySelectionOnScroll extends RecyclerView.OnScrollListener { + + private boolean doneScrolling = true; + + public void startAutoScrolling() { + doneScrolling = false; + } + + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + if (newState == SCROLL_STATE_IDLE && !doneScrolling) { + doneScrolling = true; + onScrolled(recyclerView, 0, 0); + } + } + + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (doneScrolling && recyclerView.getLayoutManager() != null && emojiPageView.getAdapter() != null) { + LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); + int index = layoutManager.findFirstCompletelyVisibleItemPosition(); + Optional> item = emojiPageView.getAdapter().getModel(index); + if (item.isPresent() && item.get() instanceof EmojiPageViewGridAdapter.HasKey) { + viewModel.selectPage(((EmojiPageViewGridAdapter.HasKey) item.get()).getKey()); + } + } + } + } + + private class SearchCallbacks implements KeyboardPageSearchView.Callbacks { + @Override + public void onQueryChanged(@NonNull String query) { + boolean hasQuery = !TextUtils.isEmpty(query); + search.enableBackNavigation(hasQuery); + if (hasQuery) { + ViewUtil.fadeOut(tabBar, 250, View.INVISIBLE); + } else { + ViewUtil.fadeIn(tabBar, 250); + } + viewModel.onQueryChanged(query); + } + + @Override + public void onNavigationClicked() { + search.clearQuery(); + search.clearFocus(); + ViewUtil.hideKeyboard(requireContext(), requireView()); + } + + @Override + public void onFocusGained() { + ((BottomSheetDialog) requireDialog()).getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED); + } + + @Override + public void onClicked() { } + + @Override + public void onFocusLost() { } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPage.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPage.java index 881b61f43a..7b75761595 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPage.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPage.java @@ -27,6 +27,10 @@ class ReactWithAnyEmojiPage { this.pageBlocks = pageBlocks; } + public @NonNull String getKey() { + return pageBlocks.get(0).getPageModel().getKey(); + } + public @StringRes int getLabel() { return pageBlocks.get(0).getLabel(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java index 013500eec1..c43f09e0a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java @@ -1,38 +1,105 @@ package org.thoughtcrime.securesms.reactions.any; +import android.text.TextUtils; + import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; +import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider; +import org.thoughtcrime.securesms.components.emoji.EmojiPageModel; +import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter; +import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.emoji.EmojiCategory; +import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoryMappingModel; +import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchRepository; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.reactions.ReactionsLoader; +import org.thoughtcrime.securesms.util.MappingModelList; +import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import java.util.List; +import java.util.stream.Collectors; public final class ReactWithAnyEmojiViewModel extends ViewModel { - private final ReactionsLoader reactionsLoader; + private static final int SEARCH_LIMIT = 40; + private final ReactWithAnyEmojiRepository repository; private final long messageId; private final boolean isMms; + private final EmojiSearchRepository emojiSearchRepository; - private final LiveData> pages; + private final LiveData categories; + private final LiveData emojiList; + private final MutableLiveData searchResults; + private final MutableLiveData selectedKey; private ReactWithAnyEmojiViewModel(@NonNull ReactionsLoader reactionsLoader, @NonNull ReactWithAnyEmojiRepository repository, long messageId, - boolean isMms) { - this.reactionsLoader = reactionsLoader; - this.repository = repository; - this.messageId = messageId; - this.isMms = isMms; - this.pages = Transformations.map(reactionsLoader.getReactions(), repository::getEmojiPageModels); + boolean isMms, + @NonNull EmojiSearchRepository emojiSearchRepository) + { + this.repository = repository; + this.messageId = messageId; + this.isMms = isMms; + this.emojiSearchRepository = emojiSearchRepository; + this.searchResults = new MutableLiveData<>(new EmojiSearchResult()); + this.selectedKey = new MutableLiveData<>(getStartingKey()); + + LiveData> emojiPages = Transformations.map(reactionsLoader.getReactions(), repository::getEmojiPageModels); + + LiveData emojiList = Transformations.map(emojiPages, (pages) -> { + MappingModelList list = new MappingModelList(); + + for (ReactWithAnyEmojiPage page : pages) { + String key = page.getKey(); + for (ReactWithAnyEmojiPageBlock block : page.getPageBlocks()) { + list.add(new EmojiPageViewGridAdapter.EmojiHeader(key, block.getLabel())); + list.addAll(toMappingModels(block.getPageModel())); + } + } + + return list; + }); + + this.categories = LiveDataUtil.combineLatest(emojiPages, this.selectedKey, (pages, selectedKey) -> { + MappingModelList list = new MappingModelList(); + list.add(new EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel(RecentEmojiPageModel.KEY.equals(selectedKey))); + list.addAll(pages.stream() + .filter(p -> !RecentEmojiPageModel.KEY.equals(p.getKey())) + .map(p -> { + EmojiCategory category = EmojiCategory.forKey(p.getKey()); + return new EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel(category, category.getKey().equals(selectedKey)); + }) + .collect(Collectors.toList())); + return list; + }); + + this.emojiList = LiveDataUtil.combineLatest(emojiList, searchResults, (all, search) -> { + if (TextUtils.isEmpty(search.query)) { + return all; + } else { + if (search.model.getDisplayEmoji().isEmpty()) { + return MappingModelList.singleton(new EmojiPageViewGridAdapter.EmojiNoResultsModel()); + } + return toMappingModels(search.model); + } + }); } - LiveData> getEmojiPageModels() { - return pages; + LiveData getCategories() { + return categories; + } + + LiveData getSelectedKey() { + return selectedKey; } void onEmojiSelected(@NonNull String emoji) { @@ -42,6 +109,51 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel { } } + LiveData getEmojiList() { + return emojiList; + } + + public void onQueryChanged(String query) { + emojiSearchRepository.submitQuery(query, false, SEARCH_LIMIT, m -> searchResults.postValue(new EmojiSearchResult(query, m))); + } + + public void selectPage(@NonNull String key) { + if (key.equals(selectedKey.getValue())) { + return; + } + + selectedKey.setValue(key); + } + + private static @NonNull MappingModelList toMappingModels(@NonNull EmojiPageModel model) { + return model.getDisplayEmoji() + .stream() + .map(e -> new EmojiPageViewGridAdapter.EmojiModel(model.getKey(), e)) + .collect(MappingModelList.collect()); + } + + private static @NonNull String getStartingKey() { + if (RecentEmojiPageModel.hasRecents(ApplicationDependencies.getApplication(), EmojiKeyboardProvider.RECENT_STORAGE_KEY)) { + return RecentEmojiPageModel.KEY; + } else { + return EmojiCategory.PEOPLE.getKey(); + } + } + + private static class EmojiSearchResult { + private final String query; + private final EmojiPageModel model; + + private EmojiSearchResult(@NonNull String query, @Nullable EmojiPageModel model) { + this.query = query; + this.model = model; + } + + public EmojiSearchResult() { + this("", null); + } + } + static class Factory implements ViewModelProvider.Factory { private final ReactionsLoader reactionsLoader; @@ -59,7 +171,7 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel { @Override public @NonNull T create(@NonNull Class modelClass) { //noinspection ConstantConditions - return modelClass.cast(new ReactWithAnyEmojiViewModel(reactionsLoader, repository, messageId, isMms)); + return modelClass.cast(new ReactWithAnyEmojiViewModel(reactionsLoader, repository, messageId, isMms, new EmojiSearchRepository(ApplicationDependencies.getApplication()))); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java index c438560524..178e28f415 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java @@ -10,6 +10,7 @@ import com.annimon.stream.Stream; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.emoji.Emoji; import org.thoughtcrime.securesms.components.emoji.EmojiPageModel; +import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel; import java.util.List; @@ -24,6 +25,11 @@ class ThisMessageEmojiPageModel implements EmojiPageModel { this.emoji = emoji; } + @Override + public String getKey() { + return RecentEmojiPageModel.KEY; + } + @Override public int getIconAttr() { return R.attr.emoji_category_recent; diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/TopAndBottomShadowHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/TopAndBottomShadowHelper.kt new file mode 100644 index 0000000000..39f8f1ed17 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/TopAndBottomShadowHelper.kt @@ -0,0 +1,76 @@ +package org.thoughtcrime.securesms.reactions.any + +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +private const val DURATION: Long = 250L + +/** + * Hide and show top and bottom shadows based on list scrolling ability. + */ +class TopAndBottomShadowHelper(private val toolbarShadow: View, private val bottomToolbarShadow: View) : RecyclerView.OnScrollListener() { + private var lastAnimationState = AnimationState.NONE + + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + val newAnimationState = getAnimationState(recyclerView) + + if (newAnimationState == lastAnimationState) { + return + } + + when (newAnimationState) { + AnimationState.NONE -> throw AssertionError() + AnimationState.HIDE_TOP_AND_HIDE_BOTTOM -> hide(toolbarShadow, bottomToolbarShadow) + AnimationState.HIDE_TOP_AND_SHOW_BOTTOM -> { + hide(toolbarShadow) + show(bottomToolbarShadow) + } + AnimationState.SHOW_TOP_AND_HIDE_BOTTOM -> { + show(toolbarShadow) + hide(bottomToolbarShadow) + } + AnimationState.SHOW_TOP_AND_SHOW_BOTTOM -> show(toolbarShadow, bottomToolbarShadow) + } + + lastAnimationState = newAnimationState + } + + private fun getAnimationState(recyclerView: RecyclerView): AnimationState { + val canScrollDown = recyclerView.canScrollVertically(1) + val canScrollUp = recyclerView.canScrollVertically(-1) + + return if (!canScrollDown && !canScrollUp) { + AnimationState.HIDE_TOP_AND_HIDE_BOTTOM + } else if (canScrollDown && !canScrollUp) { + AnimationState.HIDE_TOP_AND_SHOW_BOTTOM + } else if (!canScrollDown && canScrollUp) { + AnimationState.SHOW_TOP_AND_HIDE_BOTTOM + } else { + AnimationState.SHOW_TOP_AND_SHOW_BOTTOM + } + } + + private fun show(vararg views: View) { + views.forEach { + it.animate() + .setDuration(DURATION) + .alpha(1f) + } + } + + private fun hide(vararg views: View) { + views.forEach { + it.animate() + .setDuration(DURATION) + .alpha(0f) + } + } + + enum class AnimationState { + NONE, + HIDE_TOP_AND_HIDE_BOTTOM, + HIDE_TOP_AND_SHOW_BOTTOM, + SHOW_TOP_AND_HIDE_BOTTOM, + SHOW_TOP_AND_SHOW_BOTTOM + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/edit/EditReactionsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/reactions/edit/EditReactionsFragment.kt index df7a2d34fd..775af5eca7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/edit/EditReactionsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/edit/EditReactionsFragment.kt @@ -122,9 +122,6 @@ class EditReactionsFragment : LoggingFragment(R.layout.edit_reactions_fragment), viewModel.setSelection(EditReactionsViewModel.NO_SELECTION) } - override fun onReactWithAnyEmojiPageChanged(page: Int) { - } - override fun onReactWithAnyEmojiSelected(emoji: String) { viewModel.onEmojiSelected(emoji) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/InsetItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/util/InsetItemDecoration.kt new file mode 100644 index 0000000000..bfda0081af --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/InsetItemDecoration.kt @@ -0,0 +1,47 @@ +package org.thoughtcrime.securesms.util + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +private typealias Predicate = (view: View, parent: RecyclerView) -> Boolean +private val ALWAYS_TRUE: Predicate = { _, _ -> true } + +/** + * Externally configurable inset "setter" for recycler views. + * + * Primary constructor provides full external control of view insets. + * Secondary constructors provide basic predicate based insets on the horizontal and vertical. + */ +open class InsetItemDecoration( + private val setInset: SetInset +) : RecyclerView.ItemDecoration() { + + constructor(horizontalInset: Int = 0, verticalInset: Int = 0) : this(horizontalInset, verticalInset, ALWAYS_TRUE) + constructor(horizontalInset: Int = 0, verticalInset: Int = 0, predicate: Predicate) : this(horizontalInset, horizontalInset, verticalInset, verticalInset, predicate) + constructor(leftInset: Int = 0, rightInset: Int = 0, topInset: Int = 0, bottomInset: Int = 0, predicate: Predicate = ALWAYS_TRUE) : this( + setInset = object : SetInset() { + override fun setInset(outRect: Rect, view: View, parent: RecyclerView) { + if (predicate == ALWAYS_TRUE || predicate.invoke(view, parent)) { + outRect.left = leftInset + outRect.right = rightInset + outRect.top = topInset + outRect.bottom = bottomInset + } + } + } + ) + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + super.getItemOffsets(outRect, view, parent, state) + setInset.setInset(outRect, view, parent) + } + + abstract class SetInset { + abstract fun setInset(outRect: Rect, view: View, parent: RecyclerView) + + fun getPosition(view: View, parent: RecyclerView): Int { + return parent.getChildAdapterPosition(view) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MappingAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/util/MappingAdapter.java index dffd0b32e0..2eccc687e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MappingAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MappingAdapter.java @@ -15,8 +15,13 @@ import androidx.recyclerview.widget.RecyclerView; import org.whispersystems.libsignal.util.guava.Function; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; + +import kotlin.collections.CollectionsKt; +import kotlin.jvm.functions.Function1; /** * A reusable and composable {@link androidx.recyclerview.widget.RecyclerView.Adapter} built on-top of {@link ListAdapter} to @@ -103,6 +108,21 @@ public class MappingAdapter extends ListAdapter, MappingViewHold holder.bind(getItem(position)); } + public > int indexOfFirst(@NonNull Class clazz, @NonNull Function1 predicate) { + return CollectionsKt.indexOfFirst(getCurrentList(), m -> { + //noinspection unchecked + return clazz.isAssignableFrom(m.getClass()) && predicate.invoke((T) m); + }); + } + + public @NonNull Optional> getModel(int index) { + List> currentList = getCurrentList(); + if (index >= 0 && index < currentList.size()) { + return Optional.ofNullable(currentList.get(index)); + } + return Optional.empty(); + } + private static class MappingDiffCallback extends DiffUtil.ItemCallback> { @Override public boolean areItemsTheSame(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MappingModelList.java b/app/src/main/java/org/thoughtcrime/securesms/util/MappingModelList.java index 0413eefbeb..73f281b2c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MappingModelList.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MappingModelList.java @@ -6,11 +6,59 @@ import com.annimon.stream.Collector; import com.annimon.stream.function.BiConsumer; import com.annimon.stream.function.Function; import com.annimon.stream.function.Supplier; +import com.google.common.collect.Sets; import java.util.ArrayList; +import java.util.Collection; +import java.util.Set; +import java.util.function.BinaryOperator; public class MappingModelList extends ArrayList> { + public MappingModelList() { } + + public MappingModelList(@NonNull Collection> c) { + super(c); + } + + public static @NonNull MappingModelList singleton(@NonNull MappingModel model) { + MappingModelList list = new MappingModelList(); + list.add(model); + return list; + } + + public static @NonNull java.util.stream.Collector, MappingModelList, MappingModelList> collect() { + return new java.util.stream.Collector, MappingModelList, MappingModelList>() { + @Override + public java.util.function.Supplier supplier() { + return MappingModelList::new; + } + + @Override + public java.util.function.BiConsumer> accumulator() { + return MappingModelList::add; + } + + @Override + public BinaryOperator combiner() { + return (left, right) -> { + left.addAll(right); + return left; + }; + } + + @Override + public java.util.function.Function finisher() { + return java.util.function.Function.identity(); + } + + @Override + public Set characteristics() { + return Sets.immutableEnumSet(Characteristics.IDENTITY_FINISH); + } + }; + } + public static @NonNull Collector, MappingModelList, MappingModelList> toMappingModelList() { return new Collector, MappingModelList, MappingModelList>() { @Override diff --git a/app/src/main/res/drawable/bottom_toolbar_shadow.xml b/app/src/main/res/drawable/bottom_toolbar_shadow.xml new file mode 100644 index 0000000000..af6cf5c77c --- /dev/null +++ b/app/src/main/res/drawable/bottom_toolbar_shadow.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/keyboard_pager_fragment_category_selected.xml b/app/src/main/res/drawable/keyboard_pager_fragment_category_selected.xml index 43e24ab534..3c788fbc37 100644 --- a/app/src/main/res/drawable/keyboard_pager_fragment_category_selected.xml +++ b/app/src/main/res/drawable/keyboard_pager_fragment_category_selected.xml @@ -4,7 +4,7 @@ - + diff --git a/app/src/main/res/drawable/toolbar_shadow.xml b/app/src/main/res/drawable/toolbar_shadow.xml index 1a43a7f3b4..ce05d55a1b 100644 --- a/app/src/main/res/drawable/toolbar_shadow.xml +++ b/app/src/main/res/drawable/toolbar_shadow.xml @@ -4,5 +4,5 @@ android:angle="90" android:dither="true" android:startColor="@color/transparent_black" - android:endColor="@color/transparent_black_10" /> + android:endColor="@color/transparent_black_15" /> diff --git a/app/src/main/res/layout/emoji_display_item.xml b/app/src/main/res/layout/emoji_display_item.xml index e36f436f90..16d617f838 100644 --- a/app/src/main/res/layout/emoji_display_item.xml +++ b/app/src/main/res/layout/emoji_display_item.xml @@ -1,45 +1,18 @@ - - - - - + android:background="?selectableItemBackgroundBorderless" + tools:layout_height="60dp"> + android:id="@+id/emoji_image" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_gravity="center" + android:adjustViewBounds="true" + android:scaleType="fitCenter" + tools:src="@drawable/ic_emoji_smiley_24" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/emoji_grid_header.xml b/app/src/main/res/layout/emoji_grid_header.xml new file mode 100644 index 0000000000..a7474089e2 --- /dev/null +++ b/app/src/main/res/layout/emoji_grid_header.xml @@ -0,0 +1,13 @@ + + diff --git a/app/src/main/res/layout/emoji_grid_layout.xml b/app/src/main/res/layout/emoji_grid_layout.xml deleted file mode 100644 index 8dc8da7009..0000000000 --- a/app/src/main/res/layout/emoji_grid_layout.xml +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/emoji_grid_no_results.xml b/app/src/main/res/layout/emoji_grid_no_results.xml new file mode 100644 index 0000000000..d2eb0c3c2f --- /dev/null +++ b/app/src/main/res/layout/emoji_grid_no_results.xml @@ -0,0 +1,8 @@ + + diff --git a/app/src/main/res/layout/emoji_text_display_item.xml b/app/src/main/res/layout/emoji_text_display_item.xml new file mode 100644 index 0000000000..55db24688d --- /dev/null +++ b/app/src/main/res/layout/emoji_text_display_item.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_pager_search_bar.xml b/app/src/main/res/layout/keyboard_pager_search_bar.xml index 59ccfd9415..0701926c23 100644 --- a/app/src/main/res/layout/keyboard_pager_search_bar.xml +++ b/app/src/main/res/layout/keyboard_pager_search_bar.xml @@ -4,7 +4,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/signal_background_primary" tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> - + android:orientation="vertical"> + android:background="@color/signal_text_primary_disabled" /> - - - - - - - - + android:layout_gravity="center" + android:paddingTop="12dp" + android:paddingBottom="8dp" + app:search_bar_tint="@color/react_with_any_search_background" + app:search_hint="@string/KeyboardPagerFragment_search_emoji" + app:search_icon_tint="@color/react_with_any_search_hint" + app:show_always="true" /> - + + + + + + + + diff --git a/app/src/main/res/layout/react_with_any_emoji_dual_block_item.xml b/app/src/main/res/layout/react_with_any_emoji_dual_block_item.xml deleted file mode 100644 index 23af35ff2e..0000000000 --- a/app/src/main/res/layout/react_with_any_emoji_dual_block_item.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/react_with_any_emoji_tabs.xml b/app/src/main/res/layout/react_with_any_emoji_tabs.xml index ea90a41f27..df33155924 100644 --- a/app/src/main/res/layout/react_with_any_emoji_tabs.xml +++ b/app/src/main/res/layout/react_with_any_emoji_tabs.xml @@ -1,21 +1,26 @@ - + android:orientation="vertical"> + android:layout_height="2dp" + android:background="@drawable/bottom_toolbar_shadow" /> - - + android:orientation="horizontal" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:itemCount="10" + tools:listitem="@layout/keyboard_pager_category_icon" /> - + \ No newline at end of file diff --git a/app/src/main/res/values-night/dark_colors.xml b/app/src/main/res/values-night/dark_colors.xml index 6a1014a730..a95a7caa8e 100644 --- a/app/src/main/res/values-night/dark_colors.xml +++ b/app/src/main/res/values-night/dark_colors.xml @@ -27,7 +27,7 @@ @color/core_grey_25 @color/core_white - @color/core_grey_15 + @color/core_grey_25 @color/core_ultramarine_light @color/transparent_black_15 @@ -142,4 +142,9 @@ @color/transparent_black_40 @color/transparent_white_60 @color/core_grey_80 + + @color/core_grey_75 + @color/core_grey_65 + @color/core_grey_25 + @color/core_grey_65 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 21385c20e1..5ca342c445 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -6,7 +6,7 @@ 110dp 170dp 56dp - 48dp + 54dp 16sp 12sp 200sp diff --git a/app/src/main/res/values/light_colors.xml b/app/src/main/res/values/light_colors.xml index 0d2a75c518..69bd40a8ca 100644 --- a/app/src/main/res/values/light_colors.xml +++ b/app/src/main/res/values/light_colors.xml @@ -26,7 +26,7 @@ @color/core_grey_75 @color/core_grey_60 - @color/signal_icon_tint_primary + @color/core_grey_75 @color/core_grey_45 @color/core_ultramarine @@ -142,4 +142,9 @@ @color/transparent_white_60 @color/transparent_white_80 @color/core_grey_15 + + @color/core_white + @color/core_grey_05 + @color/core_grey_60 + @color/core_white diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1051a57c7f..97ee5ef530 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2191,6 +2191,7 @@ Symbols Flags Emoticons + No results found Use default diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 6a933992e5..40e4132dfa 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -374,10 +374,21 @@ @color/white + + + + diff --git a/app/src/test/java/org/thoughtcrime/securesms/emoji/EmojiJsonParserTest.kt b/app/src/test/java/org/thoughtcrime/securesms/emoji/EmojiJsonParserTest.kt index e2faebf469..84bf452f1b 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/emoji/EmojiJsonParserTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/emoji/EmojiJsonParserTest.kt @@ -52,25 +52,25 @@ private const val SAMPLE_JSON_WITH_OBSOLETE = """ """ private val SAMPLE_JSON_WITHOUT_OBSOLETE_EXPECTED = listOf( - StaticEmojiPageModel(EmojiCategory.FOODS.icon, listOf(Emoji("\u0001"), Emoji("\u0002", "\u0003", "\u0004")), Uri.parse("file:///Foods")), - StaticEmojiPageModel(EmojiCategory.PLACES.icon, listOf(Emoji("\ud83c\udf0d"), Emoji("\u0003", "\u0004", "\u0005")), Uri.parse("file:///Places")) + StaticEmojiPageModel(EmojiCategory.FOODS, listOf(Emoji("\u0001"), Emoji("\u0002", "\u0003", "\u0004")), Uri.parse("file:///Foods")), + StaticEmojiPageModel(EmojiCategory.PLACES, listOf(Emoji("\ud83c\udf0d"), Emoji("\u0003", "\u0004", "\u0005")), Uri.parse("file:///Places")) ) private val SAMPLE_JSON_WITH_OBSOLETE_EXPECTED_DISPLAY = listOf( - StaticEmojiPageModel(EmojiCategory.FOODS.icon, listOf(Emoji("\u0001"), Emoji("\u0002", "\u0003", "\u0004")), Uri.parse("file:///Foods")), + StaticEmojiPageModel(EmojiCategory.FOODS, listOf(Emoji("\u0001"), Emoji("\u0002", "\u0003", "\u0004")), Uri.parse("file:///Foods")), CompositeEmojiPageModel( EmojiCategory.PLACES.icon, listOf( - StaticEmojiPageModel(EmojiCategory.PLACES.icon, listOf(Emoji("\u0002"), Emoji("\u0003", "\u0004", "\u0005")), Uri.parse("file:///Places_1")), - StaticEmojiPageModel(EmojiCategory.PLACES.icon, listOf(Emoji("\u0003"), Emoji("\u0008", "\u0009", "\u0000")), Uri.parse("file:///Places_2")) + StaticEmojiPageModel(EmojiCategory.PLACES, listOf(Emoji("\u0002"), Emoji("\u0003", "\u0004", "\u0005")), Uri.parse("file:///Places_1")), + StaticEmojiPageModel(EmojiCategory.PLACES, listOf(Emoji("\u0003"), Emoji("\u0008", "\u0009", "\u0000")), Uri.parse("file:///Places_2")) ) ) ) private val SAMPLE_JSON_WITH_OBSOLETE_EXPECTED_DATA = listOf( - StaticEmojiPageModel(EmojiCategory.FOODS.icon, listOf(Emoji("\u0001"), Emoji("\u0002", "\u0003", "\u0004")), Uri.parse("file:///Foods")), - StaticEmojiPageModel(EmojiCategory.PLACES.icon, listOf(Emoji("\u0002"), Emoji("\u0003", "\u0004", "\u0005")), Uri.parse("file:///Places_1")), - StaticEmojiPageModel(EmojiCategory.PLACES.icon, listOf(Emoji("\u0003"), Emoji("\u0008", "\u0009", "\u0000")), Uri.parse("file:///Places_2")) + StaticEmojiPageModel(EmojiCategory.FOODS, listOf(Emoji("\u0001"), Emoji("\u0002", "\u0003", "\u0004")), Uri.parse("file:///Foods")), + StaticEmojiPageModel(EmojiCategory.PLACES, listOf(Emoji("\u0002"), Emoji("\u0003", "\u0004", "\u0005")), Uri.parse("file:///Places_1")), + StaticEmojiPageModel(EmojiCategory.PLACES, listOf(Emoji("\u0003"), Emoji("\u0008", "\u0009", "\u0000")), Uri.parse("file:///Places_2")) ) @RunWith(RobolectricTestRunner::class) diff --git a/app/src/test/java/org/thoughtcrime/securesms/emoji/EmojiSourceTest.kt b/app/src/test/java/org/thoughtcrime/securesms/emoji/EmojiSourceTest.kt index 4a247de577..33d270b374 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/emoji/EmojiSourceTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/emoji/EmojiSourceTest.kt @@ -20,6 +20,8 @@ class EmojiSourceTest { private class EmojiPageModelFake(private val displayE: List) : EmojiPageModel { + override fun getKey(): String = TODO("Not yet implemented") + override fun getEmoji(): List = displayE.map { it.variations }.flatten() override fun getDisplayEmoji(): List = displayE