Signal-Android/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomShee...

325 wiersze
12 KiB
Java

package org.thoughtcrime.securesms.reactions.any;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment;
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener;
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.edit.EditReactionsActivity;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
import java.util.Optional;
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
public final class ReactWithAnyEmojiBottomSheetDialogFragment extends FixedRoundedCornerBottomSheetDialogFragment implements EmojiEventListener,
EmojiPageViewGridAdapter.VariationSelectorListener
{
private static final String REACTION_STORAGE_KEY = "reactions_recent_emoji";
private static final String ABOUT_STORAGE_KEY = TextSecurePreferences.RECENT_STORAGE_KEY;
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 Callback callback;
private EmojiPageView emojiPageView;
private KeyboardPageSearchView search;
private View tabBar;
private final LifecycleDisposable disposables = new LifecycleDisposable();
private final UpdateCategorySelectionOnScroll categoryUpdateOnScroll = new UpdateCategorySelectionOnScroll();
public static DialogFragment createForStory() {
DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment();
Bundle args = new Bundle();
args.putLong(ARG_MESSAGE_ID, -1);
args.putBoolean(ARG_IS_MMS, false);
args.putInt(ARG_START_PAGE, -1);
args.putString(ARG_RECENT_KEY, REACTION_STORAGE_KEY);
args.putBoolean(ARG_EDIT, false);
fragment.setArguments(args);
return fragment;
}
public static DialogFragment createForMessageRecord(@NonNull MessageRecord messageRecord, int startingPage) {
DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment();
Bundle args = new Bundle();
args.putLong(ARG_MESSAGE_ID, messageRecord.getId());
args.putBoolean(ARG_IS_MMS, messageRecord.isMms());
args.putInt(ARG_START_PAGE, startingPage);
args.putString(ARG_RECENT_KEY, REACTION_STORAGE_KEY);
args.putBoolean(ARG_EDIT, true);
fragment.setArguments(args);
return fragment;
}
public static DialogFragment createForAboutSelection() {
DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment();
Bundle args = new Bundle();
args.putLong(ARG_MESSAGE_ID, -1);
args.putBoolean(ARG_IS_MMS, false);
args.putInt(ARG_START_PAGE, -1);
args.putString(ARG_RECENT_KEY, ABOUT_STORAGE_KEY);
fragment.setArguments(args);
return fragment;
}
public static DialogFragment createForEditReactions() {
DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment();
Bundle args = new Bundle();
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);
return fragment;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (getParentFragment() instanceof Callback) {
callback = (Callback) getParentFragment();
} else {
callback = (Callback) context;
}
}
@Override
protected int getThemeResId() {
return R.style.Widget_Signal_ReactWithAny;
}
@Override
public @NonNull Dialog onCreateDialog(Bundle savedInstanceState) {
Dialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState);
boolean shadows = requireArguments().getBoolean(ARG_SHADOWS, true);
if (!shadows) {
Window window = dialog.getWindow();
if (window != null) {
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
}
}
return dialog;
}
@Override
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);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
disposables.bindTo(getViewLifecycleOwner());
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);
initializeViewModel();
EmojiKeyboardPageCategoriesAdapter categoriesAdapter = new EmojiKeyboardPageCategoriesAdapter(key -> {
scrollTo(key);
viewModel.selectPage(key);
});
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);
customizeReactions.setOnClickListener(v -> startActivity(new Intent(requireContext(), EditReactionsActivity.class)));
}
container.addView(tabBar);
emojiPageView.addOnScrollListener(new TopAndBottomShadowHelper(requireView().findViewById(R.id.react_with_any_emoji_top_shadow),
tabBar.findViewById(R.id.react_with_any_emoji_bottom_shadow)));
disposables.add(viewModel.getEmojiList().subscribe(pages -> emojiPageView.setList(pages, null)));
disposables.add(viewModel.getCategories().subscribe(categoriesAdapter::submitList));
disposables.add(viewModel.getSelectedKey().subscribe(key -> categoriesRecycler.post(() -> {
int index = categoriesAdapter.indexOfFirst(EmojiKeyboardPageCategoryMappingModel.class, m -> m.getKey().equals(key));
if (index != -1) {
categoriesRecycler.smoothScrollToPosition(index);
}
})));
search.setCallbacks(new SearchCallbacks());
}
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);
}
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
LoaderManager.getInstance(requireActivity()).destroyLoader((int) requireArguments().getLong(ARG_MESSAGE_ID));
}
@Override
public void onDismiss(@NonNull DialogInterface dialog) {
super.onDismiss(dialog);
callback.onReactWithAnyEmojiDialogDismissed();
}
private void initializeViewModel() {
Bundle args = requireArguments();
ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext(), args.getString(ARG_RECENT_KEY, REACTION_STORAGE_KEY));
ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS));
viewModel = new ViewModelProvider(this, factory).get(ReactWithAnyEmojiViewModel.class);
}
@Override
public void onEmojiSelected(String emoji) {
viewModel.onEmojiSelected(emoji);
callback.onReactWithAnyEmojiSelected(emoji);
dismiss();
}
@Override
public void onKeyEvent(KeyEvent keyEvent) {
}
@Override
public void onVariationSelectorStateChanged(boolean open) { }
public interface Callback {
void onReactWithAnyEmojiDialogDismissed();
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() { }
}
}