From 2f4fe657e96c710651e97d5111253ea609014bc6 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sun, 9 May 2021 03:57:47 +0900 Subject: [PATCH] Allow reacting direct item with any emoji. Fixes austinhuang0131/barinsta#1137 --- .../adapters/DirectItemsAdapter.java | 2 + .../directmessages/DirectItemViewHolder.java | 4 + .../emoji/EmojiBottomSheetDialog.java | 100 +++++++++++ .../customviews/emoji/EmojiGridAdapter.java | 31 ++-- .../customviews/emoji/EmojiPopupWindow.java | 163 ------------------ .../emoji/GoogleCompatEmojiDrawable.java | 1 - .../DirectMessageThreadFragment.java | 45 +++-- .../instagrabber/managers/ThreadManager.java | 4 +- .../DirectItemEmojiReaction.java | 10 ++ .../directmessages/DirectItemReactions.java | 8 + .../instagrabber/utils/emoji/EmojiParser.java | 8 +- 11 files changed, 172 insertions(+), 204 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/customviews/emoji/EmojiBottomSheetDialog.java delete mode 100644 app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPopupWindow.java diff --git a/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java index 2b1f410e..2673d787 100644 --- a/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java @@ -406,6 +406,8 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter callback); + + void onAddReactionListener(DirectItem item); } public interface DirectItemInternalLongClickListener { diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java index 17a0956c..61e6c5cc 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java @@ -551,6 +551,10 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder imple menu.setOnDismissListener(() -> setSelected(false)); menu.setOnReactionClickListener(emoji -> callback.onReaction(item, emoji)); menu.setOnOptionSelectListener((itemId, cb) -> callback.onOptionSelect(item, itemId, cb)); + menu.setOnAddReactionListener(() -> { + menu.dismiss(); + itemView.postDelayed(() -> callback.onAddReactionListener(item), 300); + }); menu.show(itemView, location); } diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiBottomSheetDialog.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiBottomSheetDialog.java new file mode 100644 index 00000000..cdf5767c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiBottomSheetDialog.java @@ -0,0 +1,100 @@ +package awais.instagrabber.customviews.emoji; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; + +import awais.instagrabber.R; +import awais.instagrabber.customviews.helpers.GridSpacingItemDecoration; +import awais.instagrabber.utils.Utils; + +public class EmojiBottomSheetDialog extends BottomSheetDialogFragment { + public static final String TAG = EmojiBottomSheetDialog.class.getSimpleName(); + + private RecyclerView grid; + private EmojiPicker.OnEmojiClickListener callback; + + @NonNull + public static EmojiBottomSheetDialog newInstance() { + // Bundle args = new Bundle(); + // fragment.setArguments(args); + return new EmojiBottomSheetDialog(); + } + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(DialogFragment.STYLE_NORMAL, R.style.ThemeOverlay_Rounded_BottomSheetDialog); + } + + @Nullable + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + final Context context = getContext(); + if (context == null) return null; + grid = new RecyclerView(context); + return grid; + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + init(); + } + + @Override + public void onStart() { + super.onStart(); + final Dialog dialog = getDialog(); + if (dialog == null) return; + final BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialog; + final View bottomSheetInternal = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet); + if (bottomSheetInternal == null) return; + bottomSheetInternal.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + bottomSheetInternal.requestLayout(); + } + + @Override + public void onAttach(@NonNull final Context context) { + super.onAttach(context); + final Fragment parentFragment = getParentFragment(); + if (parentFragment instanceof EmojiPicker.OnEmojiClickListener) { + callback = (EmojiPicker.OnEmojiClickListener) parentFragment; + } + } + + @Override + public void onDestroyView() { + grid = null; + super.onDestroyView(); + } + + private void init() { + final Context context = getContext(); + if (context == null) return; + final GridLayoutManager gridLayoutManager = new GridLayoutManager(context, 9); + grid.setLayoutManager(gridLayoutManager); + grid.setHasFixedSize(true); + grid.setClipToPadding(false); + grid.addItemDecoration(new GridSpacingItemDecoration(Utils.convertDpToPx(8))); + final EmojiGridAdapter adapter = new EmojiGridAdapter(null, (view, emoji) -> { + if (callback != null) { + callback.onClick(view, emoji); + } + dismiss(); + }, null); + grid.setAdapter(adapter); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java index 5aff4472..1f0a7c7f 100644 --- a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java +++ b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiGridAdapter.java @@ -43,7 +43,7 @@ public class EmojiGridAdapter extends RecyclerView.Adapter { - binding.image.setImageDrawable(emoji.getDrawable()); - final boolean hasVariants = !parent.getVariants().isEmpty(); - binding.indicator.setVisibility(hasVariants ? View.VISIBLE : View.GONE); - if (onEmojiClickListener != null) { - itemView.setOnClickListener(v -> onEmojiClickListener.onClick(v, emoji)); - } - if (hasVariants && onEmojiLongClickListener != null) { - itemView.setOnLongClickListener(v -> onEmojiLongClickListener.onLongClick(position, v, parent)); - } - }); + // itemView.post(() -> { + binding.image.setImageDrawable(emoji.getDrawable()); + final boolean hasVariants = !parent.getVariants().isEmpty(); + binding.indicator.setVisibility(hasVariants ? View.VISIBLE : View.GONE); + if (onEmojiClickListener != null) { + itemView.setOnClickListener(v -> onEmojiClickListener.onClick(v, emoji)); + } + if (hasVariants && onEmojiLongClickListener != null) { + itemView.setOnLongClickListener(v -> onEmojiLongClickListener.onLongClick(position, v, parent)); + } + // }); } } diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPopupWindow.java b/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPopupWindow.java deleted file mode 100644 index 76673590..00000000 --- a/app/src/main/java/awais/instagrabber/customviews/emoji/EmojiPopupWindow.java +++ /dev/null @@ -1,163 +0,0 @@ -package awais.instagrabber.customviews.emoji; - -import android.content.Context; -import android.graphics.Rect; -import android.view.Gravity; -import android.view.View; -import android.view.WindowManager.LayoutParams; -import android.widget.PopupWindow; - -import awais.instagrabber.R; -import awais.instagrabber.customviews.emoji.EmojiPicker.OnBackspaceClickListener; -import awais.instagrabber.customviews.emoji.EmojiPicker.OnEmojiClickListener; -import awais.instagrabber.utils.Utils; - -import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; - -/** - * https://stackoverflow.com/a/33897583/1436766 - */ -public class EmojiPopupWindow extends PopupWindow { - - private int keyBoardHeight = 0; - private Boolean pendingOpen = false; - private Boolean isOpened = false; - private final View rootView; - private final Context context; - private final OnEmojiClickListener onEmojiClickListener; - private final OnBackspaceClickListener onBackspaceClickListener; - - private OnSoftKeyboardOpenCloseListener onSoftKeyboardOpenCloseListener; - - - /** - * Constructor - * - * @param rootView The top most layout in your view hierarchy. The difference of this view and the screen height will be used to calculate the keyboard height. - */ - public EmojiPopupWindow(final View rootView, - final OnEmojiClickListener onEmojiClickListener, - final OnBackspaceClickListener onBackspaceClickListener) { - super(rootView.getContext()); - this.rootView = rootView; - this.context = rootView.getContext(); - this.onEmojiClickListener = onEmojiClickListener; - this.onBackspaceClickListener = onBackspaceClickListener; - View customView = createCustomView(); - setContentView(customView); - setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); - //default size - setSize((int) context.getResources().getDimension(R.dimen.keyboard_height), MATCH_PARENT); - } - - /** - * Set the listener for the event of keyboard opening or closing. - */ - public void setOnSoftKeyboardOpenCloseListener(OnSoftKeyboardOpenCloseListener listener) { - this.onSoftKeyboardOpenCloseListener = listener; - } - - /** - * Use this function to show the emoji popup. - * NOTE: Since, the soft keyboard sizes are variable on different android devices, the - * library needs you to open the soft keyboard atleast once before calling this function. - * If that is not possible see showAtBottomPending() function. - */ - public void showAtBottom() { - showAtLocation(rootView, Gravity.BOTTOM, 0, 0); - } - - /** - * Use this function when the soft keyboard has not been opened yet. This - * will show the emoji popup after the keyboard is up next time. - * Generally, you will be calling InputMethodManager.showSoftInput function after - * calling this function. - */ - public void showAtBottomPending() { - if (isKeyBoardOpen()) - showAtBottom(); - else - pendingOpen = true; - } - - /** - * @return Returns true if the soft keyboard is open, false otherwise. - */ - public Boolean isKeyBoardOpen() { - return isOpened; - } - - /** - * Dismiss the popup - */ - @Override - public void dismiss() { - super.dismiss(); - } - - /** - * Call this function to resize the emoji popup according to your soft keyboard size - */ - public void setSizeForSoftKeyboard() { - rootView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { - Rect r = new Rect(); - rootView.getWindowVisibleDisplayFrame(r); - - int screenHeight = getUsableScreenHeight(); - int heightDifference = screenHeight - (r.bottom - r.top); - int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); - if (resourceId > 0) { - heightDifference -= context.getResources() - .getDimensionPixelSize(resourceId); - } - if (heightDifference > 100) { - keyBoardHeight = heightDifference; - setSize(MATCH_PARENT, keyBoardHeight); - if (!isOpened) { - if (onSoftKeyboardOpenCloseListener != null) - onSoftKeyboardOpenCloseListener.onKeyboardOpen(keyBoardHeight); - } - isOpened = true; - if (pendingOpen) { - showAtBottom(); - pendingOpen = false; - } - } else { - isOpened = false; - if (onSoftKeyboardOpenCloseListener != null) - onSoftKeyboardOpenCloseListener.onKeyboardClose(); - } - }); - } - - private int getUsableScreenHeight() { - return Utils.displayMetrics.heightPixels; - } - - /** - * Manually set the popup window size - * - * @param width Width of the popup - * @param height Height of the popup - */ - public void setSize(int width, int height) { - setWidth(width); - setHeight(height); - } - - private View createCustomView() { - final EmojiPicker emojiPicker = new EmojiPicker(context); - final LayoutParams layoutParams = new LayoutParams(MATCH_PARENT, MATCH_PARENT); - emojiPicker.setLayoutParams(layoutParams); - emojiPicker.init(rootView, onEmojiClickListener, onBackspaceClickListener); - return emojiPicker; - } - - - public interface OnSoftKeyboardOpenCloseListener { - void onKeyboardOpen(int keyBoardHeight); - - void onKeyboardClose(); - } -} - diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java b/app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java index f2bec738..ae1aff00 100644 --- a/app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java +++ b/app/src/main/java/awais/instagrabber/customviews/emoji/GoogleCompatEmojiDrawable.java @@ -25,7 +25,6 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.text.Spanned; import android.text.TextPaint; -import android.util.Log; import androidx.annotation.NonNull; import androidx.emoji.text.EmojiCompat; diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java index 5a97fa73..1d6b8f6e 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -15,7 +15,6 @@ import android.net.Uri; import android.os.Bundle; import android.text.Editable; import android.util.Log; -import android.util.Pair; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; @@ -33,7 +32,6 @@ import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.LiveData; -import androidx.lifecycle.MediatorLiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; @@ -75,6 +73,8 @@ import awais.instagrabber.animations.CubicBezierInterpolator; import awais.instagrabber.customviews.RecordView; import awais.instagrabber.customviews.Tooltip; import awais.instagrabber.customviews.emoji.Emoji; +import awais.instagrabber.customviews.emoji.EmojiBottomSheetDialog; +import awais.instagrabber.customviews.emoji.EmojiPicker; import awais.instagrabber.customviews.helpers.HeaderItemDecoration; import awais.instagrabber.customviews.helpers.HeightProvider; import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge; @@ -114,7 +114,8 @@ import awais.instagrabber.viewmodels.factories.DirectThreadViewModelFactory; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; -public class DirectMessageThreadFragment extends Fragment implements DirectReactionsAdapter.OnReactionClickListener { +public class DirectMessageThreadFragment extends Fragment implements DirectReactionsAdapter.OnReactionClickListener, + EmojiPicker.OnEmojiClickListener { private static final String TAG = DirectMessageThreadFragment.class.getSimpleName(); private static final int STORAGE_PERM_REQUEST_CODE = 8020; private static final int AUDIO_RECORD_PERM_REQUEST_CODE = 1000; @@ -159,6 +160,9 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private LiveData pendingRequestsCountLiveData; private LiveData> usersLiveData; private boolean autoMarkAsSeen = false; + private MenuItem markAsSeenMenuItem; + private Media tempMedia; + private DirectItem addReactionItem; private final AppExecutors appExecutors = AppExecutors.getInstance(); private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() { @@ -291,6 +295,14 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact cb.apply(item); } } + + @Override + public void onAddReactionListener(final DirectItem item) { + if (item == null) return; + addReactionItem = item; + final EmojiBottomSheetDialog emojiBottomSheetDialog = EmojiBottomSheetDialog.newInstance(); + emojiBottomSheetDialog.show(getChildFragmentManager(), EmojiBottomSheetDialog.TAG); + } }; private final DirectItemLongClickListener directItemLongClickListener = position -> { @@ -321,8 +333,6 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact backStackSavedStateResultLiveData.postValue(null); }; private final MutableLiveData inputLength = new MutableLiveData<>(0); - private MenuItem markAsSeenMenuItem; - private Media tempMedia; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { @@ -1461,25 +1471,12 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(direction); } - public static class ItemsAdapterDataMerger extends MediatorLiveData> { - private User user; - private DirectThread thread; - - public ItemsAdapterDataMerger(final LiveData userLiveData, - final LiveData threadLiveData) { - addSource(userLiveData, user -> { - this.user = user; - combine(); - }); - addSource(threadLiveData, thread -> { - this.thread = thread; - combine(); - }); - } - - private void combine() { - if (user == null || thread == null) return; - setValue(new Pair<>(user, thread)); + @Override + public void onClick(final View view, final Emoji emoji) { + if (addReactionItem == null) return; + final LiveData> resourceLiveData = viewModel.sendReaction(addReactionItem, emoji); + if (resourceLiveData != null) { + resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData)); } } } diff --git a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java index 3f2a59d1..6a81e3dc 100644 --- a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java @@ -585,8 +585,7 @@ public final class ThreadManager { if (index < 0) { temp.add(0, reaction); } else if (shouldReplaceIfAlreadyReacted) { - temp.add(0, reaction); - temp.remove(index); + temp.set(index, reaction); } return temp; } @@ -736,6 +735,7 @@ public final class ThreadManager { }); } + @NonNull public LiveData> sendReaction(final DirectItem item, final Emoji emoji) { final MutableLiveData> data = new MutableLiveData<>(); final Long userId = getCurrentUserId(data); diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemEmojiReaction.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemEmojiReaction.java index c0a8b124..068c17f6 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemEmojiReaction.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemEmojiReaction.java @@ -47,4 +47,14 @@ public class DirectItemEmojiReaction implements Serializable { public int hashCode() { return Objects.hash(senderId, timestamp, emoji, superReactType); } + + @Override + public String toString() { + return "DirectItemEmojiReaction{" + + "senderId=" + senderId + + ", timestamp=" + timestamp + + ", emoji='" + emoji + '\'' + + ", superReactType='" + superReactType + '\'' + + '}'; + } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReactions.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReactions.java index 4b344e14..30d3cf6c 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReactions.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReactions.java @@ -51,4 +51,12 @@ public class DirectItemReactions implements Cloneable, Serializable { public int hashCode() { return Objects.hash(emojis, likes); } + + @Override + public String toString() { + return "DirectItemReactions{" + + "emojis=" + emojis + + ", likes=" + likes + + '}'; + } } diff --git a/app/src/main/java/awais/instagrabber/utils/emoji/EmojiParser.java b/app/src/main/java/awais/instagrabber/utils/emoji/EmojiParser.java index ac584c34..4012688b 100644 --- a/app/src/main/java/awais/instagrabber/utils/emoji/EmojiParser.java +++ b/app/src/main/java/awais/instagrabber/utils/emoji/EmojiParser.java @@ -15,6 +15,7 @@ import java.io.InputStream; import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -77,7 +78,12 @@ public final class EmojiParser { .addAll(emoji.getVariants()) .build() .stream()) - .collect(Collectors.toMap(Emoji::getUnicode, Function.identity())); + .collect(Collectors.toMap( + Emoji::getUnicode, + Function.identity(), + (u, v) -> u, + LinkedHashMap::new + )); } catch (Exception e) { Log.e(TAG, "EmojiParser: ", e); }