Add React With Any Search and update UX.

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

View File

@@ -1,181 +0,0 @@
package org.thoughtcrime.securesms.reactions.any;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.widget.NestedScrollView;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiPageView;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter;
final class ReactWithAnyEmojiAdapter extends ListAdapter<ReactWithAnyEmojiPage, ReactWithAnyEmojiAdapter.ReactWithAnyEmojiPageViewHolder> {
private static final int VIEW_TYPE_SINGLE = 0;
private static final int VIEW_TYPE_DUAL = 1;
private final EmojiKeyboardProvider.EmojiEventListener emojiEventListener;
private final EmojiPageViewGridAdapter.VariationSelectorListener variationSelectorListener;
private final Callbacks callbacks;
ReactWithAnyEmojiAdapter(@NonNull EmojiKeyboardProvider.EmojiEventListener emojiEventListener,
@NonNull EmojiPageViewGridAdapter.VariationSelectorListener variationSelectorListener,
@NonNull Callbacks callbacks)
{
super(new PageChangedCallback());
this.emojiEventListener = emojiEventListener;
this.variationSelectorListener = variationSelectorListener;
this.callbacks = callbacks;
}
public ReactWithAnyEmojiPage getItem(int position) {
return super.getItem(position);
}
@Override
public @NonNull ReactWithAnyEmojiPageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) {
case VIEW_TYPE_SINGLE:
return new SinglePageBlockViewHolder(createEmojiPageView(parent.getContext()));
case VIEW_TYPE_DUAL:
EmojiPageView block1 = createEmojiPageView(parent.getContext());
EmojiPageView block2 = createEmojiPageView(parent.getContext());
NestedScrollView scrollView = (NestedScrollView) LayoutInflater.from(parent.getContext()).inflate(R.layout.react_with_any_emoji_dual_block_item, parent, false);
LinearLayout container = scrollView.findViewById(R.id.react_with_any_emoji_dual_block_item_container);
block1.setRecyclerNestedScrollingEnabled(false);
block2.setRecyclerNestedScrollingEnabled(false);
container.addView(block1, 0);
container.addView(block2);
return new DualPageBlockViewHolder(scrollView, block1, block2);
default:
throw new IllegalArgumentException("Unknown viewType: " + viewType);
}
}
@Override
public void onBindViewHolder(@NonNull ReactWithAnyEmojiPageViewHolder holder, int position) {
holder.bind(getItem(position));
}
@Override
public void onViewAttachedToWindow(@NonNull ReactWithAnyEmojiPageViewHolder holder) {
callbacks.onViewHolderAttached(holder.getAdapterPosition(), holder);
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
recyclerView.setNestedScrollingEnabled(false);
ViewGroup.LayoutParams params = recyclerView.getLayoutParams();
params.height = (int) (recyclerView.getResources().getDisplayMetrics().heightPixels * 0.80);
recyclerView.setLayoutParams(params);
recyclerView.setHasFixedSize(true);
}
@Override
public int getItemViewType(int position) {
return getItem(position).getPageBlocks().size() > 1 ? VIEW_TYPE_DUAL : VIEW_TYPE_SINGLE;
}
private EmojiPageView createEmojiPageView(@NonNull Context context) {
return new EmojiPageView(context, emojiEventListener, variationSelectorListener, true);
}
static abstract class ReactWithAnyEmojiPageViewHolder extends RecyclerView.ViewHolder implements ScrollableChild {
public ReactWithAnyEmojiPageViewHolder(@NonNull View itemView) {
super(itemView);
}
abstract void bind(@NonNull ReactWithAnyEmojiPage reactWithAnyEmojiPage);
}
static final class SinglePageBlockViewHolder extends ReactWithAnyEmojiPageViewHolder {
private final EmojiPageView emojiPageView;
public SinglePageBlockViewHolder(@NonNull View itemView) {
super(itemView);
emojiPageView = (EmojiPageView) itemView;
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
emojiPageView.setLayoutParams(params);
}
@Override
void bind(@NonNull ReactWithAnyEmojiPage reactWithAnyEmojiPage) {
emojiPageView.setModel(reactWithAnyEmojiPage.getPageBlocks().get(0).getPageModel());
}
@Override
public void setNestedScrollingEnabled(boolean isEnabled) {
emojiPageView.setRecyclerNestedScrollingEnabled(isEnabled);
}
}
static final class DualPageBlockViewHolder extends ReactWithAnyEmojiPageViewHolder {
private final EmojiPageView block1;
private final EmojiPageView block2;
private final TextView block2Label;
public DualPageBlockViewHolder(@NonNull View itemView,
@NonNull EmojiPageView block1,
@NonNull EmojiPageView block2)
{
super(itemView);
this.block1 = block1;
this.block2 = block2;
this.block2Label = itemView.findViewById(R.id.react_with_any_emoji_dual_block_item_block_2_label);
}
@Override
void bind(@NonNull ReactWithAnyEmojiPage reactWithAnyEmojiPage) {
block1.setModel(reactWithAnyEmojiPage.getPageBlocks().get(0).getPageModel());
block2.setModel(reactWithAnyEmojiPage.getPageBlocks().get(1).getPageModel());
block2Label.setText(reactWithAnyEmojiPage.getPageBlocks().get(1).getLabel());
}
@Override
public void setNestedScrollingEnabled(boolean isEnabled) {
((NestedScrollView) itemView).setNestedScrollingEnabled(isEnabled);
}
}
interface Callbacks {
void onViewHolderAttached(int adapterPosition, ScrollableChild pageView);
}
interface ScrollableChild {
void setNestedScrollingEnabled(boolean isEnabled);
}
private static class PageChangedCallback extends DiffUtil.ItemCallback<ReactWithAnyEmojiPage> {
@Override
public boolean areItemsTheSame(@NonNull ReactWithAnyEmojiPage oldItem, @NonNull ReactWithAnyEmojiPage newItem) {
return oldItem.getLabel() == newItem.getLabel();
}
@Override
public boolean areContentsTheSame(@NonNull ReactWithAnyEmojiPage oldItem, @NonNull ReactWithAnyEmojiPage newItem) {
return oldItem.equals(newItem);
}
}
}

View File

@@ -5,13 +5,12 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.util.SparseArray;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.TextSwitcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -20,7 +19,8 @@ import androidx.core.view.ViewCompat;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.loader.app.LoaderManager;
import androidx.viewpager2.widget.ViewPager2;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
@@ -28,21 +28,26 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.shape.CornerFamily;
import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiPageView;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoriesAdapter;
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoryMappingModel;
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView;
import org.thoughtcrime.securesms.reactions.ReactionsLoader;
import org.thoughtcrime.securesms.reactions.edit.EditReactionsActivity;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.MappingModel;
import org.thoughtcrime.securesms.util.ViewUtil;
public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomSheetDialogFragment
implements EmojiKeyboardProvider.EmojiEventListener,
EmojiPageViewGridAdapter.VariationSelectorListener
import java.util.Optional;
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomSheetDialogFragment implements EmojiKeyboardProvider.EmojiEventListener,
EmojiPageViewGridAdapter.VariationSelectorListener
{
private static final String REACTION_STORAGE_KEY = "reactions_recent_emoji";
@@ -51,20 +56,17 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
private static final String ARG_MESSAGE_ID = "arg_message_id";
private static final String ARG_IS_MMS = "arg_is_mms";
private static final String ARG_START_PAGE = "arg_start_page";
private static final String ARG_SHADOWS = "arg_shadows";
private static final String ARG_RECENT_KEY = "arg_recent_key";
private static final String ARG_EDIT = "arg_edit";
private ReactWithAnyEmojiViewModel viewModel;
private TextSwitcher categoryLabel;
private ViewPager2 categoryPager;
private ReactWithAnyEmojiAdapter adapter;
private OnPageChanged onPageChanged;
private SparseArray<ReactWithAnyEmojiAdapter.ScrollableChild> pageArray = new SparseArray<>();
private Callback callback;
private ReactionsLoader reactionsLoader;
private View customizeReactions;
private boolean showEditReactions;
private ReactWithAnyEmojiViewModel viewModel;
private Callback callback;
private ReactionsLoader reactionsLoader;
private EmojiPageView emojiPageView;
private KeyboardPageSearchView search;
private View tabBar;
private final UpdateCategorySelectionOnScroll categoryUpdateOnScroll = new UpdateCategorySelectionOnScroll();
public static DialogFragment createForMessageRecord(@NonNull MessageRecord messageRecord, int startingPage) {
DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment();
@@ -73,7 +75,6 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
args.putLong(ARG_MESSAGE_ID, messageRecord.getId());
args.putBoolean(ARG_IS_MMS, messageRecord.isMms());
args.putInt(ARG_START_PAGE, startingPage);
args.putBoolean(ARG_SHADOWS, false);
args.putString(ARG_RECENT_KEY, REACTION_STORAGE_KEY);
args.putBoolean(ARG_EDIT, true);
fragment.setArguments(args);
@@ -88,7 +89,6 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
args.putLong(ARG_MESSAGE_ID, -1);
args.putBoolean(ARG_IS_MMS, false);
args.putInt(ARG_START_PAGE, -1);
args.putBoolean(ARG_SHADOWS, true);
args.putString(ARG_RECENT_KEY, ABOUT_STORAGE_KEY);
fragment.setArguments(args);
@@ -102,7 +102,6 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
args.putLong(ARG_MESSAGE_ID, -1);
args.putBoolean(ARG_IS_MMS, false);
args.putInt(ARG_START_PAGE, -1);
args.putBoolean(ARG_SHADOWS, false);
args.putString(ARG_RECENT_KEY, REACTION_STORAGE_KEY);
fragment.setArguments(args);
@@ -122,50 +121,41 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
boolean shadows = requireArguments().getBoolean(ARG_SHADOWS);
if (ThemeUtil.isDarkTheme(requireContext())) {
setStyle(DialogFragment.STYLE_NORMAL, shadows ? R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny
: R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny_Shadowless);
} else {
setStyle(DialogFragment.STYLE_NORMAL, shadows ? R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny
: R.style.Theme_Signal_Light_BottomSheetDialog_Fixed_ReactWithAny_Shadowless);
}
super.onCreate(savedInstanceState);
setStyle(DialogFragment.STYLE_NORMAL, R.style.Widget_Signal_ReactWithAny);
}
@Override
public @NonNull Dialog onCreateDialog(Bundle savedInstanceState) {
BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
ShapeAppearanceModel shapeAppearanceModel = ShapeAppearanceModel.builder()
.setTopLeftCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 8))
.setTopRightCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 8))
.build();
MaterialShapeDrawable dialogBackground = new MaterialShapeDrawable(shapeAppearanceModel);
BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
dialog.getBehavior().setPeekHeight((int) (getResources().getDisplayMetrics().heightPixels * 0.50));
dialogBackground.setTint(ContextCompat.getColor(requireContext(), R.color.signal_background_dialog));
ShapeAppearanceModel shapeAppearanceModel = ShapeAppearanceModel.builder()
.setTopLeftCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 18))
.setTopRightCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 18))
.build();
MaterialShapeDrawable dialogBackground = new MaterialShapeDrawable(shapeAppearanceModel);
dialogBackground.setTint(ContextCompat.getColor(requireContext(), R.color.react_with_any_background));
dialog.getBehavior().addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
if (bottomSheet.getBackground() != dialogBackground) {
ViewCompat.setBackground(bottomSheet, dialogBackground);
}
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
}
public void onSlide(@NonNull View bottomSheet, float slideOffset) { }
});
return dialog;
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.react_with_any_emoji_bottom_sheet_dialog_fragment, container, false);
}
@@ -177,36 +167,14 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
LoaderManager.getInstance(requireActivity()).initLoader((int) requireArguments().getLong(ARG_MESSAGE_ID), null, reactionsLoader);
emojiPageView = view.findViewById(R.id.react_with_any_emoji_page_view);
emojiPageView.initialize(this, this, true);
emojiPageView.addOnScrollListener(categoryUpdateOnScroll);
search = view.findViewById(R.id.react_with_any_emoji_search);
search.setCallbacks(new SearchCallbacks());
initializeViewModel();
categoryLabel = view.findViewById(R.id.category_label);
categoryPager = view.findViewById(R.id.category_pager);
showEditReactions = requireArguments().getBoolean(ARG_EDIT, false);
adapter = new ReactWithAnyEmojiAdapter(this, this, (position, pageView) -> {
pageArray.put(position, pageView);
if (categoryPager.getCurrentItem() == position) {
updateFocusedRecycler(position);
}
});
onPageChanged = new OnPageChanged();
categoryPager.setAdapter(adapter);
categoryPager.registerOnPageChangeCallback(onPageChanged);
viewModel.getEmojiPageModels().observe(getViewLifecycleOwner(), pages -> {
int pageToSet = adapter.getItemCount() == 0 ? getStartingPage((pages.get(0).hasEmoji()))
: -1;
adapter.submitList(pages);
if (pageToSet >= 0) {
categoryPager.setCurrentItem(pageToSet);
}
});
}
@Override
@@ -214,32 +182,51 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
super.onActivityCreated(savedInstanceState);
if (savedInstanceState == null) {
FrameLayout container = requireDialog().findViewById(R.id.container);
LayoutInflater layoutInflater = LayoutInflater.from(requireContext());
View tabBar = layoutInflater.inflate(R.layout.react_with_any_emoji_tabs, container, false);
TabLayout categoryTabs = tabBar.findViewById(R.id.category_tabs);
EmojiKeyboardPageCategoriesAdapter categoriesAdapter = new EmojiKeyboardPageCategoriesAdapter(key -> {
scrollTo(key);
viewModel.selectPage(key);
});
customizeReactions = tabBar.findViewById(R.id.customize_reactions_frame);
if (showEditReactions) {
FrameLayout container = requireDialog().findViewById(R.id.container);
tabBar = LayoutInflater.from(requireContext())
.inflate(R.layout.react_with_any_emoji_tabs,
container,
false);
RecyclerView categoriesRecycler = tabBar.findViewById(R.id.emoji_categories_recycler);
categoriesRecycler.setAdapter(categoriesAdapter);
if (requireArguments().getBoolean(ARG_EDIT, false)) {
View customizeReactions = tabBar.findViewById(R.id.customize_reactions_frame);
customizeReactions.setVisibility(View.VISIBLE);
tabBar.findViewById(R.id.customize_reactions).setOnClickListener(v -> startActivity(new Intent(requireContext(), EditReactionsActivity.class)));
}
if (!requireArguments().getBoolean(ARG_SHADOWS)) {
View statusBarShader = layoutInflater.inflate(R.layout.react_with_any_emoji_status_fade, container, false);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewUtil.getStatusBarHeight(container));
statusBarShader.setLayoutParams(params);
container.addView(statusBarShader, 0);
customizeReactions.setOnClickListener(v -> startActivity(new Intent(requireContext(), EditReactionsActivity.class)));
}
container.addView(tabBar);
ViewCompat.setOnApplyWindowInsetsListener(container, (v, insets) -> insets.consumeSystemWindowInsets());
new TabLayoutMediator(categoryTabs, categoryPager, (tab, position) -> {
tab.setCustomView(R.layout.react_with_any_emoji_tab)
.setIcon(ThemeUtil.getThemedDrawable(requireContext(), adapter.getItem(position).getIconAttr()));
}).attach();
emojiPageView.addOnScrollListener(new TopAndBottomShadowHelper(requireView().findViewById(R.id.react_with_any_emoji_top_shadow),
tabBar.findViewById(R.id.react_with_any_emoji_bottom_shadow)));
viewModel.getEmojiList().observe(getViewLifecycleOwner(), pages -> emojiPageView.setList(pages));
viewModel.getCategories().observe(getViewLifecycleOwner(), categoriesAdapter::submitList);
viewModel.getSelectedKey().observe(getViewLifecycleOwner(), key -> categoriesRecycler.post(() -> {
int index = categoriesAdapter.indexOfFirst(EmojiKeyboardPageCategoryMappingModel.class, m -> m.getKey().equals(key));
if (index != -1) {
categoriesRecycler.smoothScrollToPosition(index);
}
}));
}
}
private void scrollTo(@NonNull String key) {
if (emojiPageView.getAdapter() != null) {
int index = emojiPageView.getAdapter().indexOfFirst(EmojiPageViewGridAdapter.EmojiHeader.class, m -> m.getKey().equals(key));
if (index != -1) {
((BottomSheetDialog) requireDialog()).getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED);
categoryUpdateOnScroll.startAutoScrolling();
emojiPageView.smoothScrollToPositionTop(index);
}
}
}
@@ -247,8 +234,6 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
public void onDestroyView() {
super.onDestroyView();
LoaderManager.getInstance(requireActivity()).destroyLoader((int) requireArguments().getLong(ARG_MESSAGE_ID));
categoryPager.unregisterOnPageChangeCallback(onPageChanged);
}
@Override
@@ -260,7 +245,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
private void initializeViewModel() {
Bundle args = requireArguments();
ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext(), args.getString(ARG_RECENT_KEY));
ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext(), args.getString(ARG_RECENT_KEY, REACTION_STORAGE_KEY));
ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(reactionsLoader, repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS));
viewModel = ViewModelProviders.of(this, factory).get(ReactWithAnyEmojiViewModel.class);
@@ -278,40 +263,72 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
}
@Override
public void onVariationSelectorStateChanged(boolean open) {
categoryPager.setUserInputEnabled(!open);
}
private void updateFocusedRecycler(int position) {
for (int i = 0; i < pageArray.size(); i++) {
pageArray.valueAt(i).setNestedScrollingEnabled(false);
}
ReactWithAnyEmojiAdapter.ScrollableChild toFocus = pageArray.get(position);
if (toFocus != null) {
toFocus.setNestedScrollingEnabled(true);
categoryPager.requestLayout();
}
categoryLabel.setText(getString(adapter.getItem(position).getLabel()));
}
private int getStartingPage(boolean firstPageHasContent) {
int startPage = requireArguments().getInt(ARG_START_PAGE);
return startPage >= 0 ? startPage : (firstPageHasContent ? 0 : 1);
}
private class OnPageChanged extends ViewPager2.OnPageChangeCallback {
@Override
public void onPageSelected(int position) {
updateFocusedRecycler(position);
callback.onReactWithAnyEmojiPageChanged(position);
}
}
public void onVariationSelectorStateChanged(boolean open) { }
public interface Callback {
void onReactWithAnyEmojiDialogDismissed();
void onReactWithAnyEmojiPageChanged(int page);
void onReactWithAnyEmojiSelected(@NonNull String emoji);
}
private class UpdateCategorySelectionOnScroll extends RecyclerView.OnScrollListener {
private boolean doneScrolling = true;
public void startAutoScrolling() {
doneScrolling = false;
}
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (newState == SCROLL_STATE_IDLE && !doneScrolling) {
doneScrolling = true;
onScrolled(recyclerView, 0, 0);
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (doneScrolling && recyclerView.getLayoutManager() != null && emojiPageView.getAdapter() != null) {
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
int index = layoutManager.findFirstCompletelyVisibleItemPosition();
Optional<MappingModel<?>> item = emojiPageView.getAdapter().getModel(index);
if (item.isPresent() && item.get() instanceof EmojiPageViewGridAdapter.HasKey) {
viewModel.selectPage(((EmojiPageViewGridAdapter.HasKey) item.get()).getKey());
}
}
}
}
private class SearchCallbacks implements KeyboardPageSearchView.Callbacks {
@Override
public void onQueryChanged(@NonNull String query) {
boolean hasQuery = !TextUtils.isEmpty(query);
search.enableBackNavigation(hasQuery);
if (hasQuery) {
ViewUtil.fadeOut(tabBar, 250, View.INVISIBLE);
} else {
ViewUtil.fadeIn(tabBar, 250);
}
viewModel.onQueryChanged(query);
}
@Override
public void onNavigationClicked() {
search.clearQuery();
search.clearFocus();
ViewUtil.hideKeyboard(requireContext(), requireView());
}
@Override
public void onFocusGained() {
((BottomSheetDialog) requireDialog()).getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED);
}
@Override
public void onClicked() { }
@Override
public void onFocusLost() { }
}
}

View File

@@ -27,6 +27,10 @@ class ReactWithAnyEmojiPage {
this.pageBlocks = pageBlocks;
}
public @NonNull String getKey() {
return pageBlocks.get(0).getPageModel().getKey();
}
public @StringRes int getLabel() {
return pageBlocks.get(0).getLabel();
}

View File

@@ -1,38 +1,105 @@
package org.thoughtcrime.securesms.reactions.any;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter;
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.emoji.EmojiCategory;
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoryMappingModel;
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchRepository;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.reactions.ReactionsLoader;
import org.thoughtcrime.securesms.util.MappingModelList;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.List;
import java.util.stream.Collectors;
public final class ReactWithAnyEmojiViewModel extends ViewModel {
private final ReactionsLoader reactionsLoader;
private static final int SEARCH_LIMIT = 40;
private final ReactWithAnyEmojiRepository repository;
private final long messageId;
private final boolean isMms;
private final EmojiSearchRepository emojiSearchRepository;
private final LiveData<List<ReactWithAnyEmojiPage>> pages;
private final LiveData<MappingModelList> categories;
private final LiveData<MappingModelList> emojiList;
private final MutableLiveData<EmojiSearchResult> searchResults;
private final MutableLiveData<String> selectedKey;
private ReactWithAnyEmojiViewModel(@NonNull ReactionsLoader reactionsLoader,
@NonNull ReactWithAnyEmojiRepository repository,
long messageId,
boolean isMms) {
this.reactionsLoader = reactionsLoader;
this.repository = repository;
this.messageId = messageId;
this.isMms = isMms;
this.pages = Transformations.map(reactionsLoader.getReactions(), repository::getEmojiPageModels);
boolean isMms,
@NonNull EmojiSearchRepository emojiSearchRepository)
{
this.repository = repository;
this.messageId = messageId;
this.isMms = isMms;
this.emojiSearchRepository = emojiSearchRepository;
this.searchResults = new MutableLiveData<>(new EmojiSearchResult());
this.selectedKey = new MutableLiveData<>(getStartingKey());
LiveData<List<ReactWithAnyEmojiPage>> emojiPages = Transformations.map(reactionsLoader.getReactions(), repository::getEmojiPageModels);
LiveData<MappingModelList> emojiList = Transformations.map(emojiPages, (pages) -> {
MappingModelList list = new MappingModelList();
for (ReactWithAnyEmojiPage page : pages) {
String key = page.getKey();
for (ReactWithAnyEmojiPageBlock block : page.getPageBlocks()) {
list.add(new EmojiPageViewGridAdapter.EmojiHeader(key, block.getLabel()));
list.addAll(toMappingModels(block.getPageModel()));
}
}
return list;
});
this.categories = LiveDataUtil.combineLatest(emojiPages, this.selectedKey, (pages, selectedKey) -> {
MappingModelList list = new MappingModelList();
list.add(new EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel(RecentEmojiPageModel.KEY.equals(selectedKey)));
list.addAll(pages.stream()
.filter(p -> !RecentEmojiPageModel.KEY.equals(p.getKey()))
.map(p -> {
EmojiCategory category = EmojiCategory.forKey(p.getKey());
return new EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel(category, category.getKey().equals(selectedKey));
})
.collect(Collectors.toList()));
return list;
});
this.emojiList = LiveDataUtil.combineLatest(emojiList, searchResults, (all, search) -> {
if (TextUtils.isEmpty(search.query)) {
return all;
} else {
if (search.model.getDisplayEmoji().isEmpty()) {
return MappingModelList.singleton(new EmojiPageViewGridAdapter.EmojiNoResultsModel());
}
return toMappingModels(search.model);
}
});
}
LiveData<List<ReactWithAnyEmojiPage>> getEmojiPageModels() {
return pages;
LiveData<MappingModelList> getCategories() {
return categories;
}
LiveData<String> getSelectedKey() {
return selectedKey;
}
void onEmojiSelected(@NonNull String emoji) {
@@ -42,6 +109,51 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
}
}
LiveData<MappingModelList> getEmojiList() {
return emojiList;
}
public void onQueryChanged(String query) {
emojiSearchRepository.submitQuery(query, false, SEARCH_LIMIT, m -> searchResults.postValue(new EmojiSearchResult(query, m)));
}
public void selectPage(@NonNull String key) {
if (key.equals(selectedKey.getValue())) {
return;
}
selectedKey.setValue(key);
}
private static @NonNull MappingModelList toMappingModels(@NonNull EmojiPageModel model) {
return model.getDisplayEmoji()
.stream()
.map(e -> new EmojiPageViewGridAdapter.EmojiModel(model.getKey(), e))
.collect(MappingModelList.collect());
}
private static @NonNull String getStartingKey() {
if (RecentEmojiPageModel.hasRecents(ApplicationDependencies.getApplication(), EmojiKeyboardProvider.RECENT_STORAGE_KEY)) {
return RecentEmojiPageModel.KEY;
} else {
return EmojiCategory.PEOPLE.getKey();
}
}
private static class EmojiSearchResult {
private final String query;
private final EmojiPageModel model;
private EmojiSearchResult(@NonNull String query, @Nullable EmojiPageModel model) {
this.query = query;
this.model = model;
}
public EmojiSearchResult() {
this("", null);
}
}
static class Factory implements ViewModelProvider.Factory {
private final ReactionsLoader reactionsLoader;
@@ -59,7 +171,7 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ReactWithAnyEmojiViewModel(reactionsLoader, repository, messageId, isMms));
return modelClass.cast(new ReactWithAnyEmojiViewModel(reactionsLoader, repository, messageId, isMms, new EmojiSearchRepository(ApplicationDependencies.getApplication())));
}
}

View File

@@ -10,6 +10,7 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.Emoji;
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
import java.util.List;
@@ -24,6 +25,11 @@ class ThisMessageEmojiPageModel implements EmojiPageModel {
this.emoji = emoji;
}
@Override
public String getKey() {
return RecentEmojiPageModel.KEY;
}
@Override
public int getIconAttr() {
return R.attr.emoji_category_recent;

View File

@@ -0,0 +1,76 @@
package org.thoughtcrime.securesms.reactions.any
import android.view.View
import androidx.recyclerview.widget.RecyclerView
private const val DURATION: Long = 250L
/**
* Hide and show top and bottom shadows based on list scrolling ability.
*/
class TopAndBottomShadowHelper(private val toolbarShadow: View, private val bottomToolbarShadow: View) : RecyclerView.OnScrollListener() {
private var lastAnimationState = AnimationState.NONE
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val newAnimationState = getAnimationState(recyclerView)
if (newAnimationState == lastAnimationState) {
return
}
when (newAnimationState) {
AnimationState.NONE -> throw AssertionError()
AnimationState.HIDE_TOP_AND_HIDE_BOTTOM -> hide(toolbarShadow, bottomToolbarShadow)
AnimationState.HIDE_TOP_AND_SHOW_BOTTOM -> {
hide(toolbarShadow)
show(bottomToolbarShadow)
}
AnimationState.SHOW_TOP_AND_HIDE_BOTTOM -> {
show(toolbarShadow)
hide(bottomToolbarShadow)
}
AnimationState.SHOW_TOP_AND_SHOW_BOTTOM -> show(toolbarShadow, bottomToolbarShadow)
}
lastAnimationState = newAnimationState
}
private fun getAnimationState(recyclerView: RecyclerView): AnimationState {
val canScrollDown = recyclerView.canScrollVertically(1)
val canScrollUp = recyclerView.canScrollVertically(-1)
return if (!canScrollDown && !canScrollUp) {
AnimationState.HIDE_TOP_AND_HIDE_BOTTOM
} else if (canScrollDown && !canScrollUp) {
AnimationState.HIDE_TOP_AND_SHOW_BOTTOM
} else if (!canScrollDown && canScrollUp) {
AnimationState.SHOW_TOP_AND_HIDE_BOTTOM
} else {
AnimationState.SHOW_TOP_AND_SHOW_BOTTOM
}
}
private fun show(vararg views: View) {
views.forEach {
it.animate()
.setDuration(DURATION)
.alpha(1f)
}
}
private fun hide(vararg views: View) {
views.forEach {
it.animate()
.setDuration(DURATION)
.alpha(0f)
}
}
enum class AnimationState {
NONE,
HIDE_TOP_AND_HIDE_BOTTOM,
HIDE_TOP_AND_SHOW_BOTTOM,
SHOW_TOP_AND_HIDE_BOTTOM,
SHOW_TOP_AND_SHOW_BOTTOM
}
}

View File

@@ -122,9 +122,6 @@ class EditReactionsFragment : LoggingFragment(R.layout.edit_reactions_fragment),
viewModel.setSelection(EditReactionsViewModel.NO_SELECTION)
}
override fun onReactWithAnyEmojiPageChanged(page: Int) {
}
override fun onReactWithAnyEmojiSelected(emoji: String) {
viewModel.onEmojiSelected(emoji)
}