Add React With Any Search and update UX.
This commit is contained in:
parent
da2ee33dff
commit
2a1e5e4471
52 changed files with 1014 additions and 608 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -22,4 +22,8 @@ public class Emoji {
|
|||
public List<String> getVariations() {
|
||||
return variations;
|
||||
}
|
||||
|
||||
public boolean hasMultipleVariations() {
|
||||
return variations.size() > 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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())));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
8
app/src/main/res/drawable/bottom_toolbar_shadow.xml
Normal file
8
app/src/main/res/drawable/bottom_toolbar_shadow.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
13
app/src/main/res/layout/emoji_grid_header.xml
Normal file
13
app/src/main/res/layout/emoji_grid_header.xml
Normal 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" />
|
|
@ -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" />
|
8
app/src/main/res/layout/emoji_grid_no_results.xml
Normal file
8
app/src/main/res/layout/emoji_grid_no_results.xml
Normal 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" />
|
16
app/src/main/res/layout/emoji_text_display_item.xml
Normal file
16
app/src/main/res/layout/emoji_text_display_item.xml
Normal 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>
|
|
@ -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
|
||||
|
|
|
@ -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 & 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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue