mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 09:49:30 +01:00
Add React With Any Search and update UX.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user