Add React With Any Search and update UX.

This commit is contained in:
Cody Henthorne 2021-06-24 15:14:34 -04:00
parent da2ee33dff
commit 2a1e5e4471
52 changed files with 1014 additions and 608 deletions

View file

@ -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<EmojiPageModel> models;
@AttrRes private final int iconAttr;
@NonNull private final List<EmojiPageModel> models;
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull List<EmojiPageModel> models) {
this.iconAttr = iconAttr;
this.models = models;
}
@Override
public String getKey() {
return Util.hasItems(models) ? models.get(0).getKey() : "";
}
public int getIconAttr() {
return iconAttr;
}

View file

@ -22,4 +22,8 @@ public class Emoji {
public List<String> getVariations() {
return variations;
}
public boolean hasMultipleVariations() {
return variations.size() > 1;
}
}

View file

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

View file

@ -7,6 +7,7 @@ import androidx.annotation.Nullable;
import java.util.List;
public interface EmojiPageModel {
String getKey();
int getIconAttr();
List<String> getEmoji();
List<Emoji> getDisplayEmoji();

View file

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

View file

@ -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<EmojiModel> {
public static class EmojiHeader implements MappingModel<EmojiHeader>, 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<EmojiHeader> {
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<EmojiModel>, 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<EmojiTextModel>, 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<EmojiTextModel> {
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<EmojiNoResultsModel> {
@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);
}

View file

@ -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<String> emojiSet) {
String[] emojis = new String[emojiSet.size()];
int i = emojiSet.size() - 1;
for (String emoji : emojiSet) {
emojis[i--] = emoji;
}
return emojis;
}
}

View file

@ -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> emoji;
@Nullable private final Uri sprite;
private final @NonNull EmojiCategory category;
private final @NonNull List<Emoji> emoji;
private final @Nullable Uri sprite;
public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull String[] strings, @Nullable Uri sprite) {
List<Emoji> 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> emoji, @Nullable Uri sprite) {
this.category = category;
this.emoji = Collections.unmodifiableList(emoji);
this.sprite = sprite;
}
public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull List<Emoji> 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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<EmojiPageModel>): List<EmojiPageModel> {

View file

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

View file

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

View file

@ -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<T : KeyboardPageCategoryIconMappingModel<T>> : MappingModel<T> {
val key: String
@ -15,14 +16,14 @@ interface KeyboardPageCategoryIconMappingModel<T : KeyboardPageCategoryIconMappi
fun getIcon(context: Context): Drawable
}
class KeyboardPageCategoryIconViewHolder<T : KeyboardPageCategoryIconMappingModel<T>>(itemView: View, private val onPageSelected: (String) -> Unit) : MappingViewHolder<T>(itemView) {
class KeyboardPageCategoryIconViewHolder<T : KeyboardPageCategoryIconMappingModel<T>>(itemView: View, private val onPageSelected: Consumer<String>) : MappingViewHolder<T>(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))

View file

@ -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<String>) : MappingAdapter() {
init {
registerFactory(EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel::class.java, LayoutFactory({ v -> KeyboardPageCategoryIconViewHolder<EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel>(v, onPageSelected) }, R.layout.keyboard_pager_category_icon))
registerFactory(EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel::class.java, LayoutFactory({ v -> KeyboardPageCategoryIconViewHolder<EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel>(v, onPageSelected) }, R.layout.keyboard_pager_category_icon))

View file

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

View file

@ -20,7 +20,7 @@ class EmojiKeyboardPageViewModel : ViewModel() {
val categories: LiveData<MappingModelList> = 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
}

View file

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

View file

@ -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<EmojiPageModel>) {
if (query.length < MINIMUM_QUERY_THRESHOLD && includeRecents) {
consumer.accept(RecentEmojiPageModel(context, EmojiKeyboardProvider.RECENT_STORAGE_KEY))
} else {
SignalExecutors.SERIAL.execute {
val emoji: List<String> = emojiSearchDatabase.query(query, EMOJI_SEARCH_LIMIT)
val emoji: List<String> = emojiSearchDatabase.query(query, limit)
val displayEmoji: List<Emoji> = 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<String>,
private val displayEmoji: List<Emoji>
) : EmojiPageModel {
override fun getKey(): String = ""
override fun getIconAttr(): Int = -1
override fun getEmoji(): List<String> = emoji

View file

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

View file

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

View file

@ -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<ReactWithAnyEmojiPage, ReactWithAnyEmojiAdapter.ReactWithAnyEmojiPageViewHolder> {
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<ReactWithAnyEmojiPage> {
@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);
}
}
}

View file

@ -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<ReactWithAnyEmojiAdapter.ScrollableChild> 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<MappingModel<?>> 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() { }
}
}

View file

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

View file

@ -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<List<ReactWithAnyEmojiPage>> pages;
private final LiveData<MappingModelList> categories;
private final LiveData<MappingModelList> emojiList;
private final MutableLiveData<EmojiSearchResult> searchResults;
private final MutableLiveData<String> 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<List<ReactWithAnyEmojiPage>> emojiPages = Transformations.map(reactionsLoader.getReactions(), repository::getEmojiPageModels);
LiveData<MappingModelList> 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<List<ReactWithAnyEmojiPage>> getEmojiPageModels() {
return pages;
LiveData<MappingModelList> getCategories() {
return categories;
}
LiveData<String> getSelectedKey() {
return selectedKey;
}
void onEmojiSelected(@NonNull String emoji) {
@ -42,6 +109,51 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
}
}
LiveData<MappingModelList> 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 extends ViewModel> T create(@NonNull Class<T> 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())));
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<MappingModel<?>, MappingViewHold
holder.bind(getItem(position));
}
public <T extends MappingModel<T>> int indexOfFirst(@NonNull Class<T> clazz, @NonNull Function1<T, Boolean> predicate) {
return CollectionsKt.indexOfFirst(getCurrentList(), m -> {
//noinspection unchecked
return clazz.isAssignableFrom(m.getClass()) && predicate.invoke((T) m);
});
}
public @NonNull Optional<MappingModel<?>> getModel(int index) {
List<MappingModel<?>> currentList = getCurrentList();
if (index >= 0 && index < currentList.size()) {
return Optional.ofNullable(currentList.get(index));
}
return Optional.empty();
}
private static class MappingDiffCallback extends DiffUtil.ItemCallback<MappingModel<?>> {
@Override
public boolean areItemsTheSame(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) {

View file

@ -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<MappingModel<?>> {
public MappingModelList() { }
public MappingModelList(@NonNull Collection<? extends MappingModel<?>> 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<MappingModel<?>, MappingModelList, MappingModelList> collect() {
return new java.util.stream.Collector<MappingModel<?>, MappingModelList, MappingModelList>() {
@Override
public java.util.function.Supplier<MappingModelList> supplier() {
return MappingModelList::new;
}
@Override
public java.util.function.BiConsumer<MappingModelList, MappingModel<?>> accumulator() {
return MappingModelList::add;
}
@Override
public BinaryOperator<MappingModelList> combiner() {
return (left, right) -> {
left.addAll(right);
return left;
};
}
@Override
public java.util.function.Function<MappingModelList, MappingModelList> finisher() {
return java.util.function.Function.identity();
}
@Override
public Set<Characteristics> characteristics() {
return Sets.immutableEnumSet(Characteristics.IDENTITY_FINISH);
}
};
}
public static @NonNull Collector<MappingModel<?>, MappingModelList, MappingModelList> toMappingModelList() {
return new Collector<MappingModel<?>, MappingModelList, MappingModelList>() {
@Override

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="-90"
android:dither="true"
android:startColor="@color/transparent_black"
android:endColor="@color/transparent_black_15" />
</shape>

View file

@ -4,7 +4,7 @@
<layer-list>
<item android:top="8dp" android:left="8dp" android:bottom="8dp" android:right="8dp">
<shape android:shape="rectangle">
<solid android:color="@color/signal_inverse_transparent_05" />
<solid android:color="@color/signal_inverse_transparent_08" />
<corners android:radius="8dp" />
</shape>
</item>

View file

@ -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" />
</shape>

View file

@ -1,45 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:padding="2dp">
<ImageView
android:id="@+id/emoji_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:paddingStart="6dp"
android:paddingTop="6dp"
android:paddingEnd="6dp"
android:scaleType="fitCenter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_emoji_smiley_24" />
<org.thoughtcrime.securesms.components.emoji.AsciiEmojiView
android:id="@+id/emoji_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="6dp"
android:paddingTop="6dp"
android:paddingEnd="6dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:background="?selectableItemBackgroundBorderless"
tools:layout_height="60dp">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/emoji_variation_hint"
android:layout_width="7dp"
android:layout_height="7dp"
android:tint="@color/core_grey_25"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:srcCompat="@drawable/triangle_bottom_right_corner" />
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" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/emoji_grid_header_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:gravity="center_vertical"
android:minHeight="48dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.Signal.Body1.Bold"
android:textColor="@color/signal_text_primary"
tools:text="@string/ReactWithAnyEmojiBottomSheetDialogFragment__activities" />

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/emoji"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="8dp" />

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/ReactWithAnyEmojiBottomSheetDialogFragment__no_results_found"
android:textAppearance="@style/TextAppearance.Signal.Body1"
android:textColor="@color/signal_text_secondary" />

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
tools:layout_height="60dp">
<org.thoughtcrime.securesms.components.emoji.AsciiEmojiView
android:id="@+id/emoji_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>

View file

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

View file

@ -1,59 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/react_with_any_emoji_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minHeight="@dimen/react_with_any_emoji_bottom_sheet_dialog_fragment_min_height">
android:orientation="vertical">
<View
android:id="@+id/react_with_any_emoji_pull_bar"
android:layout_width="48dp"
android:layout_height="2dp"
android:background="@color/signal_text_primary_disabled"
android:layout_gravity="center_horizontal"
android:layout_marginTop="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
android:background="@color/signal_text_primary_disabled" />
<TextSwitcher
android:id="@+id/category_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="18dp"
android:layout_marginEnd="10dp"
android:inAnimation="@anim/fade_in"
android:outAnimation="@anim/fade_out"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/react_with_any_emoji_pull_bar">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Signal.Subtitle2"
android:textColor="@color/signal_icon_tint_primary"
tools:text="Smileys &amp; People" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Signal.Subtitle2"
android:textColor="@color/signal_icon_tint_primary" />
</TextSwitcher>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/category_pager"
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
android:id="@+id/react_with_any_emoji_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginBottom="@dimen/react_with_any_emoji_bottom_sheet_dialog_fragment_tabs_height"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/category_label"
app:layout_constraintVertical_bias="0" />
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" />
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:minHeight="1000dp">
<org.thoughtcrime.securesms.components.emoji.EmojiPageView
android:id="@+id/react_with_any_emoji_page_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="-2dp"
android:clipToPadding="false"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:paddingBottom="64dp" />
<View
android:id="@+id/react_with_any_emoji_top_shadow"
android:layout_width="match_parent"
android:layout_height="2dp"
android:alpha="0"
android:background="@drawable/toolbar_shadow" />
</FrameLayout>
</LinearLayout>

View file

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/react_with_any_emoji_dual_block_item_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/react_with_any_emoji_dual_block_item_block_2_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="10dp"
android:textAppearance="@style/TextAppearance.Signal.Subtitle2"
android:textColor="@color/signal_text_secondary" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -1,21 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@color/signal_background_dialog">
android:orientation="vertical">
<View
android:id="@+id/react_with_any_emoji_bottom_shadow"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_gravity="top"
android:background="@color/signal_inverse_transparent_05" />
android:layout_height="2dp"
android:background="@drawable/bottom_toolbar_shadow" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="?actionBarSize"
android:layout_gravity="bottom"
android:background="@color/signal_background_dialog"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal">
<FrameLayout
@ -26,7 +31,8 @@
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_background_dialog"
android:backgroundTint="@color/react_with_any_customize_background"
android:clipChildren="false"
android:elevation="1dp"
android:visibility="gone"
tools:ignore="UnusedAttribute"
@ -37,30 +43,25 @@
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/EditReactionsFragment__customize_reactions"
android:foreground="?attr/selectableItemBackground"
android:padding="5dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_settings_24"
app:tint="@color/signal_icon_tint_tab_selected"
app:tint="@color/signal_icon_tint_tab_unselected"
tools:ignore="UnusedAttribute" />
</FrameLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/category_tabs"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/emoji_categories_recycler"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
android:layout_height="wrap_content"
android:layout_weight="1"
app:tabIndicatorColor="@color/transparent"
app:tabMaxWidth="48dp"
app:tabMode="scrollable"
app:tabPadding="0dp"
app:tabPaddingBottom="0dp"
app:tabPaddingEnd="0dp"
app:tabPaddingStart="0dp"
app:tabPaddingTop="0dp" />
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="10"
tools:listitem="@layout/keyboard_pager_category_icon" />
</LinearLayout>
</FrameLayout>
</LinearLayout>

View file

@ -27,7 +27,7 @@
<color name="signal_icon_tint_secondary">@color/core_grey_25</color>
<color name="signal_icon_tint_tab_selected">@color/core_white</color>
<color name="signal_icon_tint_tab_unselected">@color/core_grey_15</color>
<color name="signal_icon_tint_tab_unselected">@color/core_grey_25</color>
<color name="signal_button_primary">@color/core_ultramarine_light</color>
<color name="signal_button_primary_ripple">@color/transparent_black_15</color>
@ -142,4 +142,9 @@
<color name="quote_view_background">@color/transparent_black_40</color>
<color name="conversation_item_outgoing_footer_fg">@color/transparent_white_60</color>
<color name="quote_preview_background">@color/core_grey_80</color>
<color name="react_with_any_background">@color/core_grey_75</color>
<color name="react_with_any_search_background">@color/core_grey_65</color>
<color name="react_with_any_search_hint">@color/core_grey_25</color>
<color name="react_with_any_customize_background">@color/core_grey_65</color>
</resources>

View file

@ -6,7 +6,7 @@
<dimen name="min_custom_keyboard_size">110dp</dimen>
<dimen name="min_custom_keyboard_top_margin_portrait">170dp</dimen>
<dimen name="min_custom_keyboard_top_margin_landscape_bubble">56dp</dimen>
<dimen name="emoji_drawer_item_width">48dp</dimen>
<dimen name="emoji_drawer_item_width">54dp</dimen>
<dimen name="conversation_item_body_text_size">16sp</dimen>
<dimen name="conversation_item_date_text_size">12sp</dimen>
<dimen name="transport_selection_popup_width">200sp</dimen>

View file

@ -26,7 +26,7 @@
<color name="signal_icon_tint_primary">@color/core_grey_75</color>
<color name="signal_icon_tint_secondary">@color/core_grey_60</color>
<color name="signal_icon_tint_tab_selected">@color/signal_icon_tint_primary</color>
<color name="signal_icon_tint_tab_selected">@color/core_grey_75</color>
<color name="signal_icon_tint_tab_unselected">@color/core_grey_45</color>
<color name="signal_button_primary">@color/core_ultramarine</color>
@ -142,4 +142,9 @@
<color name="quote_view_background">@color/transparent_white_60</color>
<color name="conversation_item_outgoing_footer_fg">@color/transparent_white_80</color>
<color name="quote_preview_background">@color/core_grey_15</color>
<color name="react_with_any_background">@color/core_white</color>
<color name="react_with_any_search_background">@color/core_grey_05</color>
<color name="react_with_any_search_hint">@color/core_grey_60</color>
<color name="react_with_any_customize_background">@color/core_white</color>
</resources>

View file

@ -2191,6 +2191,7 @@
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__symbols">Symbols</string>
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__flags">Flags</string>
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__emoticons">Emoticons</string>
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__no_results_found">No results found</string>
<!-- arrays.xml -->
<string name="arrays__use_default">Use default</string>

View file

@ -374,10 +374,21 @@
<item name="backgroundTint">@color/white</item>
</style>
<style name="Widget.Signal.ReactWithAny" parent="ThemeOverlay.MaterialComponents.BottomSheetDialog">
<item name="android:windowIsFloating">false</item>
<item name="android:windowSoftInputMode">adjustResize</item>
<item name="bottomSheetStyle">@style/Widget.Signal.ReactWithAny.BottomSheet</item>
</style>
<style name="Widget.Signal.ReactWithAny.BottomSheet" parent="Widget.MaterialComponents.BottomSheet">
<item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.Signal.BottomSheet.Rounded</item>
<item name="backgroundTint">@color/react_with_any_background</item>
</style>
<style name="ShapeAppearanceOverlay.Signal.BottomSheet.Rounded" parent="">
<item name="cornerFamily">rounded</item>
<item name="cornerSizeTopRight">8dp</item>
<item name="cornerSizeTopLeft">8dp</item>
<item name="cornerSizeTopRight">18dp</item>
<item name="cornerSizeTopLeft">18dp</item>
<item name="cornerSizeBottomLeft">0dp</item>
<item name="cornerSizeBottomRight">0dp</item>
</style>

View file

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

View file

@ -20,6 +20,8 @@ class EmojiSourceTest {
private class EmojiPageModelFake(private val displayE: List<Emoji>) : EmojiPageModel {
override fun getKey(): String = TODO("Not yet implemented")
override fun getEmoji(): List<String> = displayE.map { it.variations }.flatten()
override fun getDisplayEmoji(): List<Emoji> = displayE