From cf62d88531f7dc7032071785f1f8dfb883fc16c2 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 13 Jan 2021 22:22:25 +0900 Subject: [PATCH] Add like/reaction (WIP) --- .../adapters/DirectItemsAdapter.java | 44 +- .../viewholder/DirectInboxItemViewHolder.java | 18 +- .../DirectItemLinkViewHolder.java | 1 + .../DirectItemMediaShareViewHolder.java | 31 +- .../directmessages/DirectItemViewHolder.java | 184 +++++-- .../DirectItemVoiceMediaViewHolder.java | 1 + .../animations/RevealOutlineAnimation.java | 84 ++++ .../RoundedRectRevealOutlineProvider.java | 56 +++ .../customviews/ChatMessageLayout.java | 3 +- .../customviews/DirectItemContextMenu.java | 457 ++++++++++++++++++ .../customviews/DirectItemFrameLayout.java | 110 +++++ .../instagrabber/customviews/PopupDialog.java | 55 --- .../customviews/emoji/ReactionsManager.java | 76 +++ .../DirectMessageThreadFragment.java | 36 +- .../ReactionBroadcastOptions.java | 7 + .../responses/directmessages/DirectItem.java | 6 +- .../DirectItemEmojiReaction.java | 18 + .../directmessages/DirectItemReactions.java | 39 +- .../awais/instagrabber/utils/Constants.java | 1 + .../instagrabber/utils/SettingsHelper.java | 3 +- .../instagrabber/utils/emoji/EmojiParser.java | 7 + .../viewmodels/DirectThreadViewModel.java | 114 +++++ .../webservices/DirectMessagesService.java | 10 + .../main/res/drawable/bg_rounded_corner.xml | 12 + app/src/main/res/drawable/ic_add.xml | 10 +- .../fragment_direct_messages_thread.xml | 10 + app/src/main/res/layout/item_pref_divider.xml | 2 +- .../res/layout/layout_direct_item_options.xml | 69 +++ app/src/main/res/layout/layout_dm_base.xml | 70 ++- .../main/res/layout/layout_dm_media_share.xml | 5 +- app/src/main/res/values/dimens.xml | 10 + app/src/main/res/values/ids.xml | 5 + app/src/main/res/values/strings.xml | 1 + 33 files changed, 1406 insertions(+), 149 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/animations/RevealOutlineAnimation.java create mode 100644 app/src/main/java/awais/instagrabber/animations/RoundedRectRevealOutlineProvider.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/DirectItemFrameLayout.java delete mode 100644 app/src/main/java/awais/instagrabber/customviews/PopupDialog.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/emoji/ReactionsManager.java create mode 100644 app/src/main/res/drawable/bg_rounded_corner.xml create mode 100644 app/src/main/res/layout/layout_direct_item_options.xml create mode 100644 app/src/main/res/values/ids.xml diff --git a/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java index 7f4622f7..5b57bf2f 100644 --- a/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java @@ -15,6 +15,7 @@ import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Objects; import awais.instagrabber.adapters.viewholder.directmessages.DirectItemActionLogViewHolder; import awais.instagrabber.adapters.viewholder.directmessages.DirectItemAnimatedMediaViewHolder; @@ -32,6 +33,7 @@ import awais.instagrabber.adapters.viewholder.directmessages.DirectItemTextViewH import awais.instagrabber.adapters.viewholder.directmessages.DirectItemVideoCallEventViewHolder; import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder; import awais.instagrabber.adapters.viewholder.directmessages.DirectItemVoiceMediaViewHolder; +import awais.instagrabber.customviews.emoji.Emoji; import awais.instagrabber.databinding.LayoutDmActionLogBinding; import awais.instagrabber.databinding.LayoutDmAnimatedMediaBinding; import awais.instagrabber.databinding.LayoutDmBaseBinding; @@ -57,11 +59,14 @@ import awais.instagrabber.utils.DateUtils; public final class DirectItemsAdapter extends RecyclerView.Adapter { private static final String TAG = DirectItemsAdapter.class.getSimpleName(); - private final User currentUser; + private List items; private DirectThread thread; + private DirectItemViewHolder selectedViewHolder; + + private final User currentUser; private final DirectItemCallback callback; private final AsyncListDiffer differ; - private List items; + private final DirectItemInternalLongClickListener longClickListener; private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() { @Override @@ -96,21 +101,30 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter(new AdapterListUpdateCallback(this), new AsyncDifferConfig.Builder<>(diffCallback).build()); - // this.onClickListener = onClickListener; - // this.mentionClickListener = mentionClickListener; + longClickListener = (position, viewHolder) -> { + if (selectedViewHolder != null) { + selectedViewHolder.setSelected(false); + } + selectedViewHolder = viewHolder; + viewHolder.setSelected(true); + itemLongClickListener.onLongClick(position); + }; } @NonNull @@ -123,7 +137,9 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter openURL(linkContext.getLinkUrl()); binding.preview.setOnClickListener(onClickListener); + // binding.preview.setOnLongClickListener(v -> itemView.performLongClick()); binding.title.setOnClickListener(onClickListener); binding.summary.setOnClickListener(onClickListener); binding.url.setOnClickListener(onClickListener); diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java index 72f6144e..796e2a74 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemMediaShareViewHolder.java @@ -12,6 +12,8 @@ import com.facebook.drawee.drawable.ScalingUtils; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.generic.RoundingParams; +import java.util.Objects; + import awais.instagrabber.R; import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback; import awais.instagrabber.databinding.LayoutDmBaseBinding; @@ -29,6 +31,7 @@ import awais.instagrabber.utils.NumberUtils; import awais.instagrabber.utils.ResponseBodyUtils; public class DirectItemMediaShareViewHolder extends DirectItemViewHolder { + private static final String TAG = DirectItemMediaShareViewHolder.class.getSimpleName(); private final LayoutDmMediaShareBinding binding; private final RoundingParams incomingRoundingParams; @@ -48,11 +51,6 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder { @Override public void bindItem(final DirectItem item, final MessageDirection messageDirection) { - final RoundingParams roundingParams = messageDirection == MessageDirection.INCOMING ? incomingRoundingParams : outgoingRoundingParams; - binding.mediaPreview.setHierarchy(new GenericDraweeHierarchyBuilder(itemView.getResources()) - .setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP) - .setRoundingParams(roundingParams) - .build()); binding.topBg.setBackgroundResource(messageDirection == MessageDirection.INCOMING ? R.drawable.bg_media_share_top_incoming : R.drawable.bg_media_share_top_outgoing); @@ -67,10 +65,10 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder { final MediaItemType mediaType = media.getMediaType(); setupTypeIndicator(mediaType); if (mediaType == MediaItemType.MEDIA_TYPE_SLIDER) { - setupPreview(media.getCarouselMedia().get(0)); + setupPreview(media.getCarouselMedia().get(0), messageDirection); return; } - setupPreview(media); + setupPreview(media, messageDirection); }); itemView.setOnClickListener(v -> openMedia(media)); } @@ -87,7 +85,17 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder { } } - private void setupPreview(@NonNull final Media media) { + private void setupPreview(@NonNull final Media media, + final MessageDirection messageDirection) { + final String url = ResponseBodyUtils.getThumbUrl(media.getImageVersions2()); + if (Objects.equals(url, binding.mediaPreview.getTag())) { + return; + } + final RoundingParams roundingParams = messageDirection == MessageDirection.INCOMING ? incomingRoundingParams : outgoingRoundingParams; + binding.mediaPreview.setHierarchy(new GenericDraweeHierarchyBuilder(itemView.getResources()) + .setActualImageScaleType(ScalingUtils.ScaleType.CENTER_CROP) + .setRoundingParams(roundingParams) + .build()); final Pair widthHeight = NumberUtils.calculateWidthHeight( media.getOriginalHeight(), media.getOriginalWidth(), @@ -98,7 +106,7 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder { layoutParams.width = widthHeight.first != null ? widthHeight.first : 0; layoutParams.height = widthHeight.second != null ? widthHeight.second : 0; binding.mediaPreview.requestLayout(); - final String url = ResponseBodyUtils.getThumbUrl(media.getImageVersions2()); + binding.mediaPreview.setTag(url); binding.mediaPreview.setImageURI(url); } @@ -153,4 +161,9 @@ public class DirectItemMediaShareViewHolder extends DirectItemViewHolder { } return media; } + + @Override + protected int getReactionsTranslationY() { + return reactionTranslationYType2; + } } 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 072e9b8c..5a5003c1 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 @@ -1,18 +1,27 @@ package awais.instagrabber.adapters.viewholder.directmessages; +import android.annotation.SuppressLint; import android.content.res.ColorStateList; import android.content.res.Resources; +import android.graphics.Point; import android.graphics.drawable.Drawable; import android.text.format.DateFormat; import android.view.Gravity; import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewPropertyAnimator; +import android.view.animation.AccelerateDecelerateInterpolator; import android.widget.FrameLayout; +import androidx.annotation.CallSuper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.widget.ImageViewCompat; import androidx.recyclerview.widget.RecyclerView; +import androidx.transition.TransitionManager; + +import com.google.android.material.transition.MaterialFade; import java.util.List; import java.util.Locale; @@ -20,6 +29,9 @@ import java.util.stream.Collectors; import awais.instagrabber.R; import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback; +import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemInternalLongClickListener; +import awais.instagrabber.customviews.DirectItemContextMenu; +import awais.instagrabber.customviews.DirectItemFrameLayout; import awais.instagrabber.customviews.RamboTextViewV2; import awais.instagrabber.databinding.LayoutDmBaseBinding; import awais.instagrabber.models.enums.DirectItemType; @@ -43,6 +55,8 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { private final int groupMessageWidth; private final List userIds; private final DirectItemCallback callback; + private final int reactionAdjustMargin; + private final AccelerateDecelerateInterpolator accelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator(); protected final int margin; protected final int dmRadius; @@ -51,6 +65,14 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { protected final int mediaImageMaxHeight; protected final int windowWidth; protected final int mediaImageMaxWidth; + protected final int reactionTranslationYType1; + protected final int reactionTranslationYType2; + + private boolean selected = false; + private DirectItemInternalLongClickListener longClickListener; + private DirectItem item; + private ViewPropertyAnimator shrinkGrowAnimator; + // private View.OnLayoutChangeListener layoutChangeListener; public DirectItemViewHolder(@NonNull final LayoutDmBaseBinding binding, @NonNull final User currentUser, @@ -75,16 +97,24 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { messageInfoPaddingSmall = resources.getDimensionPixelSize(R.dimen.dm_message_info_padding_small); windowWidth = resources.getDisplayMetrics().widthPixels; mediaImageMaxHeight = resources.getDimensionPixelSize(R.dimen.dm_media_img_max_height); + reactionAdjustMargin = resources.getDimensionPixelSize(R.dimen.dm_reaction_adjust_margin); final int groupWidthCorrection = avatarSize + messageInfoPaddingSmall * 3; - mediaImageMaxWidth = windowWidth - margin - (thread.isGroup() ? groupWidthCorrection : 0); + mediaImageMaxWidth = windowWidth - margin - (thread.isGroup() ? groupWidthCorrection : messageInfoPaddingSmall * 2); // messageInfoPaddingSmall is used cuz it's also 4dp, 1 avatar margin + 2 paddings = 3 groupMessageWidth = windowWidth - margin - groupWidthCorrection; + reactionTranslationYType1 = resources.getDimensionPixelSize(R.dimen.dm_reaction_translation_y_type_1); + reactionTranslationYType2 = resources.getDimensionPixelSize(R.dimen.dm_reaction_translation_y_type_2); } - public void bind(final DirectItem item) { + public void bind(final int position, final DirectItem item) { + this.item = item; final MessageDirection messageDirection = isSelf(item) ? MessageDirection.OUTGOING : MessageDirection.INCOMING; itemView.post(() -> bindBase(item, messageDirection)); itemView.post(() -> bindItem(item, messageDirection)); + itemView.post(() -> setupLongClickListener(position)); + // bindBase(item, messageDirection); + // bindItem(item, messageDirection); + // setupLongClickListener(position); } private void bindBase(final DirectItem item, final MessageDirection messageDirection) { @@ -104,7 +134,7 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { binding.messageInfo.setPadding(0, 0, messageInfoPaddingSmall, dmRadiusSmall); } setupReply(item, messageDirection); - setReactions(item, thread.getUsers()); + setReactions(item); } private void setBackground(final MessageDirection messageDirection) { @@ -304,31 +334,39 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { replyInfoLayoutParams.endToStart = isIncoming ? ConstraintLayout.LayoutParams.UNSET : quoteLineId; } - private void setReactions(final DirectItem item, final List users) { - final DirectItemReactions reactions = item.getReactions(); - final List emojis = reactions != null ? reactions.getEmojis() : null; - if (emojis == null || emojis.isEmpty()) { - binding.reactions.setVisibility(View.GONE); - return; - } - binding.reactions.setVisibility(View.VISIBLE); - final String emojisJoined = emojis.stream() - .map(DirectItemEmojiReaction::getEmoji) - .collect(Collectors.joining()); - final String text = String.format(Locale.ENGLISH, "%s %d", emojisJoined, emojis.size()); - binding.emojis.setText(text); - // final List reactedUsers = emojis.stream() - // .map(DirectItemEmojiReaction::getSenderId) - // .distinct() - // .map(userId -> getUser(userId, users)) - // .collect(Collectors.toList()); - // for (final DirectUser user : reactedUsers) { - // if (user == null) continue; - // final ProfilePicView profilePicView = new ProfilePicView(itemView.getContext()); - // profilePicView.setSize(ProfilePicView.Size.TINY); - // profilePicView.setImageURI(user.getProfilePicUrl()); - // binding.reactions.addView(profilePicView); - // } + private void setReactions(final DirectItem item) { + binding.getRoot().post(() -> { + MaterialFade materialFade = new MaterialFade(); + materialFade.addTarget(binding.emojis); + TransitionManager.beginDelayedTransition(binding.getRoot(), materialFade); + final DirectItemReactions reactions = item.getReactions(); + final List emojis = reactions != null ? reactions.getEmojis() : null; + if (emojis == null || emojis.isEmpty()) { + binding.container.setPadding(messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall); + binding.emojis.setVisibility(View.GONE); + return; + } + binding.emojis.setVisibility(View.VISIBLE); + binding.emojis.setTranslationY(getReactionsTranslationY()); + binding.container.setPadding(messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall, reactionAdjustMargin); + final String emojisJoined = emojis.stream() + .map(DirectItemEmojiReaction::getEmoji) + .collect(Collectors.joining()); + final String text = String.format(Locale.ENGLISH, "%s %d", emojisJoined, emojis.size()); + binding.emojis.setText(text); + // final List reactedUsers = emojis.stream() + // .map(DirectItemEmojiReaction::getSenderId) + // .distinct() + // .map(userId -> getUser(userId, users)) + // .collect(Collectors.toList()); + // for (final DirectUser user : reactedUsers) { + // if (user == null) continue; + // final ProfilePicView profilePicView = new ProfilePicView(itemView.getContext()); + // profilePicView.setSize(ProfilePicView.Size.TINY); + // profilePicView.setImageURI(user.getProfilePicUrl()); + // binding.reactions.addView(profilePicView); + // } + }); } protected boolean isSelf(final DirectItem directItem) { @@ -370,7 +408,28 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { return true; } - public void cleanup() {} + protected boolean allowLongClick() { + return true; + } + + protected boolean allowReaction() { + return true; + } + + protected List getLongClickOptions() { + return null; + } + + protected int getReactionsTranslationY() { + return reactionTranslationYType1; + } + + @CallSuper + public void cleanup() { + // if (layoutChangeListener != null) { + // binding.container.removeOnLayoutChangeListener(layoutChangeListener); + // } + } protected void setupRamboTextListeners(@NonNull final RamboTextViewV2 textView) { textView.addOnHashtagListener(autoLinkItem -> callback.onHashtagClick(autoLinkItem.getOriginalText().trim())); @@ -410,6 +469,73 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { } } + @SuppressLint("ClickableViewAccessibility") + private void setupLongClickListener(final int position) { + if (!allowLongClick()) return; + binding.getRoot().setOnItemLongClickListener(new DirectItemFrameLayout.OnItemLongClickListener() { + @Override + public void onLongClickStart(final View view) { + itemView.post(() -> shrink()); + } + + @Override + public void onLongClickCancel(final View view) { + itemView.post(() -> grow()); + } + + @Override + public void onLongClick(final View view, final float x, final float y) { + // if (longClickListener == null) return false; + // longClickListener.onLongClick(position, this); + itemView.post(() -> grow()); + setSelected(true); + showLongClickOptions(new Point((int) x, (int) y)); + } + }); + } + + private void showLongClickOptions(final Point location) { + final DirectItemContextMenu menu = new DirectItemContextMenu(itemView.getContext(), allowReaction(), getLongClickOptions()); + menu.setOnDismissListener(() -> setSelected(false)); + menu.setOnReactionClickListener(emoji -> { + callback.onReaction(item, emoji); + }); + menu.show(itemView, location); + } + + public void setLongClickListener(final DirectItemInternalLongClickListener longClickListener) { + this.longClickListener = longClickListener; + } + + public void setSelected(final boolean selected) { + this.selected = selected; + } + + private void shrink() { + if (shrinkGrowAnimator != null) { + shrinkGrowAnimator.cancel(); + } + shrinkGrowAnimator = itemView.animate() + .scaleX(0.8f) + .scaleY(0.8f) + .setInterpolator(accelerateDecelerateInterpolator) + .setDuration(ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout()); + shrinkGrowAnimator.start(); + } + + private void grow() { + if (shrinkGrowAnimator != null) { + shrinkGrowAnimator.cancel(); + } + shrinkGrowAnimator = itemView.animate() + .scaleX(1f) + .scaleY(1f) + .setInterpolator(accelerateDecelerateInterpolator) + .setDuration(200) + .withEndAction(() -> shrinkGrowAnimator = null); + shrinkGrowAnimator.start(); + } + public enum MessageDirection { INCOMING, OUTGOING diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemVoiceMediaViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemVoiceMediaViewHolder.java index b26bfa90..3b6411ea 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemVoiceMediaViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemVoiceMediaViewHolder.java @@ -154,6 +154,7 @@ public class DirectItemVoiceMediaViewHolder extends DirectItemViewHolder { @Override public void cleanup() { + super.cleanup(); if (handler != null && positionChecker != null) { handler.removeCallbacks(positionChecker); handler = null; diff --git a/app/src/main/java/awais/instagrabber/animations/RevealOutlineAnimation.java b/app/src/main/java/awais/instagrabber/animations/RevealOutlineAnimation.java new file mode 100644 index 00000000..16661719 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/animations/RevealOutlineAnimation.java @@ -0,0 +1,84 @@ +package awais.instagrabber.animations; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.graphics.Outline; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewOutlineProvider; + +/** + * A {@link ViewOutlineProvider} that has helper functions to create reveal animations. + * This class should be extended so that subclasses can define the reveal shape as the + * animation progresses from 0 to 1. + */ +public abstract class RevealOutlineAnimation extends ViewOutlineProvider { + protected Rect mOutline; + protected float mOutlineRadius; + + public RevealOutlineAnimation() { + mOutline = new Rect(); + } + + /** + * Returns whether elevation should be removed for the duration of the reveal animation. + */ + abstract boolean shouldRemoveElevationDuringAnimation(); + + /** + * Sets the progress, from 0 to 1, of the reveal animation. + */ + abstract void setProgress(float progress); + + public ValueAnimator createRevealAnimator(final View revealView, boolean isReversed) { + ValueAnimator va = + isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f); + final float elevation = revealView.getElevation(); + + va.addListener(new AnimatorListenerAdapter() { + private boolean mIsClippedToOutline; + private ViewOutlineProvider mOldOutlineProvider; + + public void onAnimationStart(Animator animation) { + mIsClippedToOutline = revealView.getClipToOutline(); + mOldOutlineProvider = revealView.getOutlineProvider(); + + revealView.setOutlineProvider(RevealOutlineAnimation.this); + revealView.setClipToOutline(true); + if (shouldRemoveElevationDuringAnimation()) { + revealView.setTranslationZ(-elevation); + } + } + + public void onAnimationEnd(Animator animation) { + revealView.setOutlineProvider(mOldOutlineProvider); + revealView.setClipToOutline(mIsClippedToOutline); + if (shouldRemoveElevationDuringAnimation()) { + revealView.setTranslationZ(0); + } + } + + }); + + va.addUpdateListener(v -> { + float progress = (Float) v.getAnimatedValue(); + setProgress(progress); + revealView.invalidateOutline(); + }); + return va; + } + + @Override + public void getOutline(View v, Outline outline) { + outline.setRoundRect(mOutline, mOutlineRadius); + } + + public float getRadius() { + return mOutlineRadius; + } + + public void getOutline(Rect out) { + out.set(mOutline); + } +} diff --git a/app/src/main/java/awais/instagrabber/animations/RoundedRectRevealOutlineProvider.java b/app/src/main/java/awais/instagrabber/animations/RoundedRectRevealOutlineProvider.java new file mode 100644 index 00000000..c5cf0439 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/animations/RoundedRectRevealOutlineProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package awais.instagrabber.animations; + +import android.graphics.Rect; + +/** + * A {@link RevealOutlineAnimation} that provides an outline that interpolates between two radii + * and two {@link Rect}s. + *

+ * An example usage of this provider is an outline that starts out as a circle and ends + * as a rounded rectangle. + */ +public class RoundedRectRevealOutlineProvider extends RevealOutlineAnimation { + private final float mStartRadius; + private final float mEndRadius; + + private final Rect mStartRect; + private final Rect mEndRect; + + public RoundedRectRevealOutlineProvider(float startRadius, float endRadius, Rect startRect, Rect endRect) { + mStartRadius = startRadius; + mEndRadius = endRadius; + mStartRect = startRect; + mEndRect = endRect; + } + + @Override + public boolean shouldRemoveElevationDuringAnimation() { + return false; + } + + @Override + public void setProgress(float progress) { + mOutlineRadius = (1 - progress) * mStartRadius + progress * mEndRadius; + + mOutline.left = (int) ((1 - progress) * mStartRect.left + progress * mEndRect.left); + mOutline.top = (int) ((1 - progress) * mStartRect.top + progress * mEndRect.top); + mOutline.right = (int) ((1 - progress) * mStartRect.right + progress * mEndRect.right); + mOutline.bottom = (int) ((1 - progress) * mStartRect.bottom + progress * mEndRect.bottom); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java b/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java index 26cb242a..1731dbf3 100644 --- a/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java +++ b/app/src/main/java/awais/instagrabber/customviews/ChatMessageLayout.java @@ -93,7 +93,8 @@ public class ChatMessageLayout extends FrameLayout { widthSize += viewPartMainWidth; heightSize += viewPartMainHeight; } else if (firstChildId == R.id.raven_media_container || firstChildId == R.id.profile_container || firstChildId == R.id.voice_media - || firstChildId == R.id.story_container || firstChildId == R.id.media_share_container || firstChildId == R.id.link_container) { + || firstChildId == R.id.story_container || firstChildId == R.id.media_share_container || firstChildId == R.id.link_container + || firstChildId == R.id.ivAnimatedMessage) { widthSize += viewPartMainWidth; heightSize += viewPartMainHeight + viewPartInfoHeight; } else { diff --git a/app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java b/app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java new file mode 100644 index 00000000..f125f72a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/DirectItemContextMenu.java @@ -0,0 +1,457 @@ +package awais.instagrabber.customviews; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.widget.ImageView; +import android.widget.PopupWindow; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.widget.AppCompatEditText; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.appcompat.widget.AppCompatTextView; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.util.Pair; + +import java.util.List; + +import awais.instagrabber.R; +import awais.instagrabber.animations.RoundedRectRevealOutlineProvider; +import awais.instagrabber.customviews.emoji.Emoji; +import awais.instagrabber.customviews.emoji.ReactionsManager; +import awais.instagrabber.databinding.LayoutDirectItemOptionsBinding; + +import static android.view.View.MeasureSpec.makeMeasureSpec; + +public class DirectItemContextMenu extends PopupWindow { + private static final String TAG = DirectItemContextMenu.class.getSimpleName(); + private static final int DO_NOT_UPDATE_FLAG = -1; + private static final int DURATION = 300; + + private final Context context; + private final boolean showReactions; + private final ReactionsManager reactionsManager; + private final int emojiSize; + private final int emojiMargin; + private final int emojiMarginHalf; + private final Rect startRect = new Rect(); + private final Rect endRect = new Rect(); + private final TimeInterpolator revealInterpolator = new AccelerateDecelerateInterpolator(); + private final AnimatorListenerAdapter exitAnimationListener; + private final TypedValue selectableItemBackgroundBorderless; + private final TypedValue selectableItemBackground; + private final int dividerHeight; + private final int optionHeight; + private final int optionPadding; + private final int addAdjust; + private final boolean hasOptions; + private final List options; + + /* = ImmutableList.of( + new MenuItem(R.id.reply, R.string.reply), + new MenuItem(R.id.unsend, R.string.dms_inbox_unsend) + );*/ + private AnimatorSet openCloseAnimator; + private Point location; + private Point point; + private OnReactionClickListener onReactionClickListener; + private OnOptionSelectListener onOptionSelectListener; + private OnAddReactionClickListener onAddReactionListener; + + public DirectItemContextMenu(@NonNull final Context context, final boolean showReactions, final List options) { + super(context); + this.context = context; + this.showReactions = showReactions; + this.options = options; + if (!showReactions && (options == null || options.isEmpty())) { + throw new IllegalArgumentException("showReactions is set false and options are empty"); + } + reactionsManager = ReactionsManager.getInstance(); + emojiSize = context.getResources().getDimensionPixelSize(R.dimen.reaction_picker_emoji_size); + emojiMargin = context.getResources().getDimensionPixelSize(R.dimen.reaction_picker_emoji_margin); + emojiMarginHalf = emojiMargin / 2; + addAdjust = context.getResources().getDimensionPixelSize(R.dimen.reaction_picker_add_padding_adjustment); + dividerHeight = context.getResources().getDimensionPixelSize(R.dimen.horizontal_divider_height); + optionHeight = context.getResources().getDimensionPixelSize(R.dimen.reaction_picker_option_height); + optionPadding = context.getResources().getDimensionPixelSize(R.dimen.dm_message_card_radius); + exitAnimationListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(final Animator animation) { + openCloseAnimator = null; + point = null; + getContentView().post(DirectItemContextMenu.super::dismiss); + } + }; + selectableItemBackgroundBorderless = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, selectableItemBackgroundBorderless, true); + selectableItemBackground = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, selectableItemBackground, true); + hasOptions = options != null && !options.isEmpty(); + } + + public void show(@NonNull View rootView, @NonNull final Point location) { + final View content = createContentView(); + content.measure(makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); + setup(content); + // rootView.getParent().requestDisallowInterceptTouchEvent(true); + // final Point correctedLocation = new Point(location.x, location.y - emojiSize * 2); + this.location = location; + showAtLocation(rootView, Gravity.TOP | Gravity.START, location.x, location.y); + // fixPopupLocation(popupWindow, correctedLocation); + animateOpen(); + } + + private void setup(final View content) { + setContentView(content); + setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + setFocusable(true); + setOutsideTouchable(true); + setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); + setBackgroundDrawable(null); + } + + public void setOnOptionSelectListener(final OnOptionSelectListener onOptionSelectListener) { + this.onOptionSelectListener = onOptionSelectListener; + } + + public void setOnReactionClickListener(final OnReactionClickListener onReactionClickListener) { + this.onReactionClickListener = onReactionClickListener; + } + + public void setOnAddReactionListener(final OnAddReactionClickListener onAddReactionListener) { + this.onAddReactionListener = onAddReactionListener; + } + + private void animateOpen() { + final View contentView = getContentView(); + contentView.setVisibility(View.INVISIBLE); + contentView.post(() -> { + final AnimatorSet openAnim = new AnimatorSet(); + // Rectangular reveal. + final ValueAnimator revealAnim = createOpenCloseOutlineProvider().createRevealAnimator(contentView, false); + revealAnim.setDuration(DURATION); + revealAnim.setInterpolator(revealInterpolator); + + ValueAnimator fadeIn = ValueAnimator.ofFloat(0, 1); + fadeIn.setDuration(DURATION); + fadeIn.setInterpolator(revealInterpolator); + fadeIn.addUpdateListener(anim -> { + float alpha = (float) anim.getAnimatedValue(); + contentView.setAlpha(revealAnim.isStarted() ? alpha : 0); + }); + openAnim.play(fadeIn); + openAnim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + contentView.setAlpha(1f); + openCloseAnimator = null; + } + }); + + openCloseAnimator = openAnim; + openAnim.playSequentially(revealAnim); + contentView.setVisibility(View.VISIBLE); + openAnim.start(); + }); + } + + protected void animateClose() { + endRect.setEmpty(); + if (openCloseAnimator != null) { + openCloseAnimator.cancel(); + } + final View contentView = getContentView(); + final AnimatorSet closeAnim = new AnimatorSet(); + // Rectangular reveal (reversed). + final ValueAnimator revealAnim = createOpenCloseOutlineProvider().createRevealAnimator(contentView, true); + revealAnim.setDuration(DURATION); + revealAnim.setInterpolator(revealInterpolator); + closeAnim.play(revealAnim); + + ValueAnimator fadeOut = ValueAnimator.ofFloat(contentView.getAlpha(), 0); + fadeOut.setDuration(DURATION); + fadeOut.setInterpolator(revealInterpolator); + fadeOut.addUpdateListener(anim -> { + float alpha = (float) anim.getAnimatedValue(); + contentView.setAlpha(revealAnim.isStarted() ? alpha : contentView.getAlpha()); + }); + closeAnim.playTogether(fadeOut); + closeAnim.addListener(exitAnimationListener); + openCloseAnimator = closeAnim; + closeAnim.start(); + } + + private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { + final View contentView = getContentView(); + final int radius = context.getResources().getDimensionPixelSize(R.dimen.dm_message_card_radius_small); + // Log.d(TAG, "createOpenCloseOutlineProvider: " + locationOnScreen(contentView) + " " + contentView.getMeasuredWidth() + " " + contentView + // .getMeasuredHeight()); + if (point == null) { + point = locationOnScreen(contentView); + } + final int left = location.x - point.x; + final int top = location.y - point.y; + startRect.set(left, top, left, top); + endRect.set(0, 0, contentView.getMeasuredWidth(), contentView.getMeasuredHeight()); + return new RoundedRectRevealOutlineProvider(radius, radius, startRect, endRect); + } + + public void dismiss() { + animateClose(); + } + + private View createContentView() { + final LayoutInflater layoutInflater = LayoutInflater.from(context); + final LayoutDirectItemOptionsBinding binding = LayoutDirectItemOptionsBinding.inflate(layoutInflater, null, false); + Pair firstLastEmojiView = null; + if (showReactions) { + firstLastEmojiView = addReactions(layoutInflater, binding.container); + } + if (hasOptions) { + View divider = null; + if (showReactions) { + if (firstLastEmojiView == null) { + throw new IllegalStateException("firstLastEmojiView is null even though reactions were added"); + } + // add divider if reactions were added + divider = addDivider(binding.container, + firstLastEmojiView.first.getId(), + firstLastEmojiView.first.getId(), + firstLastEmojiView.second.getId()); + ((ConstraintLayout.LayoutParams) firstLastEmojiView.first.getLayoutParams()).bottomToTop = divider.getId(); + } + addOptions(layoutInflater, binding.container, divider); + } + return binding.getRoot(); + } + + private Pair addReactions(final LayoutInflater layoutInflater, final ConstraintLayout container) { + final List reactions = reactionsManager.getReactions(); + AppCompatImageView prevSquareImageView = null; + View firstImageView = null; + View lastImageView = null; + for (int i = 0; i < reactions.size(); i++) { + final Emoji reaction = reactions.get(i); + final AppCompatImageView imageView = getEmojiImageView(); + final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) imageView.getLayoutParams(); + if (i == 0 && !hasOptions) { + // only connect bottom to parent bottom if there are no options + layoutParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; + } + if (i == 0) { + layoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; + layoutParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID; + firstImageView = imageView; + layoutParams.setMargins(emojiMargin, emojiMargin, emojiMarginHalf, emojiMargin); + } else { + layoutParams.startToEnd = prevSquareImageView.getId(); + final ConstraintLayout.LayoutParams prevViewLayoutParams = (ConstraintLayout.LayoutParams) prevSquareImageView.getLayoutParams(); + prevViewLayoutParams.endToStart = imageView.getId(); + // always connect the other image view's top and bottom to the first image view top and bottom + layoutParams.topToTop = firstImageView.getId(); + layoutParams.bottomToBottom = firstImageView.getId(); + layoutParams.setMargins(emojiMarginHalf, emojiMargin, emojiMarginHalf, emojiMargin); + } + imageView.setImageDrawable(reaction.getDrawable()); + imageView.setOnClickListener(view -> { + if (onReactionClickListener != null) { + onReactionClickListener.onClick(reaction); + } + dismiss(); + }); + container.addView(imageView); + prevSquareImageView = imageView; + } + // add the + icon + if (prevSquareImageView != null) { + final AppCompatImageView imageView = getEmojiImageView(); + final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) imageView.getLayoutParams(); + layoutParams.topToTop = firstImageView.getId(); + layoutParams.bottomToBottom = firstImageView.getId(); + layoutParams.startToEnd = prevSquareImageView.getId(); + final ConstraintLayout.LayoutParams prevViewLayoutParams = (ConstraintLayout.LayoutParams) prevSquareImageView.getLayoutParams(); + prevViewLayoutParams.endToStart = imageView.getId(); + layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID; + layoutParams.setMargins(emojiMarginHalf - addAdjust, emojiMargin - addAdjust, emojiMargin - addAdjust, emojiMargin - addAdjust); + imageView.setImageResource(R.drawable.ic_add); + imageView.setOnClickListener(view -> { + if (onAddReactionListener != null) { + onAddReactionListener.onAdd(); + } + dismiss(); + }); + lastImageView = imageView; + container.addView(imageView); + } + return new Pair<>(firstImageView, lastImageView); + } + + @NonNull + private AppCompatImageView getEmojiImageView() { + final AppCompatImageView imageView = new AppCompatImageView(context); + final ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(emojiSize, emojiSize); + imageView.setBackgroundResource(selectableItemBackgroundBorderless.resourceId); + imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); + imageView.setId(SquareImageView.generateViewId()); + imageView.setLayoutParams(layoutParams); + return imageView; + } + + private void addOptions(final LayoutInflater layoutInflater, + final ConstraintLayout container, + @Nullable final View divider) { + View prevOptionView = null; + for (int i = 0; i < options.size(); i++) { + final MenuItem menuItem = options.get(i); + final AppCompatTextView textView = getTextView(); + final ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) textView.getLayoutParams(); + layoutParams.startToStart = ConstraintLayout.LayoutParams.PARENT_ID; + layoutParams.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID; + if (i == 0) { + if (divider != null) { + layoutParams.topToBottom = divider.getId(); + } else { + // if divider is null mean reactions were not added, so connect top to top of parent + layoutParams.topToTop = ConstraintLayout.LayoutParams.PARENT_ID; + } + ((ConstraintLayout.LayoutParams) divider.getLayoutParams()).bottomToTop = textView.getId(); + } else { + layoutParams.topToBottom = prevOptionView.getId(); + final ConstraintLayout.LayoutParams prevLayoutParams = (ConstraintLayout.LayoutParams) prevOptionView.getLayoutParams(); + prevLayoutParams.bottomToTop = textView.getId(); + } + if (i == options.size() - 1) { + layoutParams.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; + layoutParams.bottomMargin = emojiMargin; // material design spec (https://material.io/components/menus#specs) + } + textView.setText(context.getString(menuItem.getTitleRes())); + textView.setOnClickListener(v -> { + if (onOptionSelectListener != null) { + onOptionSelectListener.onSelect(menuItem.getItemId()); + } + dismiss(); + }); + container.addView(textView); + prevOptionView = textView; + } + } + + private AppCompatTextView getTextView() { + final AppCompatTextView textView = new AppCompatTextView(context); + textView.setId(AppCompatEditText.generateViewId()); + textView.setBackgroundResource(selectableItemBackground.resourceId); + textView.setGravity(Gravity.CENTER_VERTICAL); + textView.setPaddingRelative(optionPadding, 0, optionPadding, 0); + textView.setTextAppearance(context, R.style.TextAppearance_MaterialComponents_Body1); + final ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_CONSTRAINT, + optionHeight); + textView.setLayoutParams(layoutParams); + return textView; + } + + private View addDivider(final ConstraintLayout container, + final int topViewId, + final int startViewId, + final int endViewId) { + final View dividerView = new View(context); + dividerView.setId(View.generateViewId()); + dividerView.setBackgroundResource(R.drawable.pref_list_divider_material); + final ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_CONSTRAINT, + dividerHeight); + layoutParams.topToBottom = topViewId; + layoutParams.startToStart = startViewId; + layoutParams.endToEnd = endViewId; + dividerView.setLayoutParams(layoutParams); + container.addView(dividerView); + return dividerView; + } + + @NonNull + private Point locationOnScreen(@NonNull final View view) { + final int[] location = new int[2]; + view.getLocationOnScreen(location); + return new Point(location[0], location[1]); + } + + public static class MenuItem { + @IdRes + private final int itemId; + @StringRes + private final int titleRes; + + public MenuItem(@IdRes final int itemId, @StringRes final int titleRes) { + this.itemId = itemId; + this.titleRes = titleRes; + } + + public int getItemId() { + return itemId; + } + + public int getTitleRes() { + return titleRes; + } + } + + public interface OnOptionSelectListener { + void onSelect(int itemId); + } + + public interface OnReactionClickListener { + void onClick(Emoji emoji); + } + + public interface OnAddReactionClickListener { + void onAdd(); + } + + // @NonNull + // private Rect getGlobalVisibleRect(@NonNull final View view) { + // final Rect rect = new Rect(); + // view.getGlobalVisibleRect(rect); + // return rect; + // } + + // private void fixPopupLocation(@NonNull final PopupWindow popupWindow, @NonNull final Point desiredLocation) { + // popupWindow.getContentView().post(() -> { + // final Point actualLocation = locationOnScreen(popupWindow.getContentView()); + // + // if (!(actualLocation.x == desiredLocation.x && actualLocation.y == desiredLocation.y)) { + // final int differenceX = actualLocation.x - desiredLocation.x; + // final int differenceY = actualLocation.y - desiredLocation.y; + // + // final int fixedOffsetX; + // final int fixedOffsetY; + // + // if (actualLocation.x > desiredLocation.x) { + // fixedOffsetX = desiredLocation.x - differenceX; + // } else { + // fixedOffsetX = desiredLocation.x + differenceX; + // } + // + // if (actualLocation.y > desiredLocation.y) { + // fixedOffsetY = desiredLocation.y - differenceY; + // } else { + // fixedOffsetY = desiredLocation.y + differenceY; + // } + // + // popupWindow.update(fixedOffsetX, fixedOffsetY, DO_NOT_UPDATE_FLAG, DO_NOT_UPDATE_FLAG); + // } + // }); + // } +} + diff --git a/app/src/main/java/awais/instagrabber/customviews/DirectItemFrameLayout.java b/app/src/main/java/awais/instagrabber/customviews/DirectItemFrameLayout.java new file mode 100644 index 00000000..09ca687b --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/DirectItemFrameLayout.java @@ -0,0 +1,110 @@ +package awais.instagrabber.customviews; + +import android.content.Context; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class DirectItemFrameLayout extends FrameLayout { + private static final String TAG = DirectItemFrameLayout.class.getSimpleName(); + + private boolean longPressed = false; + private float touchX; + private float touchY; + private OnItemLongClickListener onItemLongClickListener; + private int touchSlop; + + private final Handler handler = new Handler(); + private final Runnable longPressRunnable = () -> { + longPressed = true; + if (onItemLongClickListener != null) { + onItemLongClickListener.onLongClick(this, touchX, touchY); + } + }; + private final Runnable longPressStartRunnable = () -> { + if (onItemLongClickListener != null) { + onItemLongClickListener.onLongClickStart(this); + } + }; + + public DirectItemFrameLayout(@NonNull final Context context) { + super(context); + init(context); + } + + public DirectItemFrameLayout(@NonNull final Context context, @Nullable final AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public DirectItemFrameLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + public DirectItemFrameLayout(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context); + } + + private void init(final Context context) { + ViewConfiguration vc = ViewConfiguration.get(context); + touchSlop = vc.getScaledTouchSlop(); + } + + public void setOnItemLongClickListener(final OnItemLongClickListener onItemLongClickListener) { + this.onItemLongClickListener = onItemLongClickListener; + } + + @Override + public boolean dispatchTouchEvent(final MotionEvent ev) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + longPressed = false; + handler.postDelayed(longPressRunnable, ViewConfiguration.getLongPressTimeout()); + handler.postDelayed(longPressStartRunnable, ViewConfiguration.getTapTimeout()); + touchX = ev.getRawX(); + touchY = ev.getRawY(); + break; + case MotionEvent.ACTION_MOVE: + if (longPressed || Math.abs(touchX - ev.getRawX()) > touchSlop || Math.abs(touchY - ev.getRawY()) > touchSlop) { + handler.removeCallbacks(longPressStartRunnable); + handler.removeCallbacks(longPressRunnable); + } + break; + case MotionEvent.ACTION_UP: + handler.removeCallbacks(longPressRunnable); + handler.removeCallbacks(longPressStartRunnable); + if (longPressed) { + return true; + } + if (onItemLongClickListener != null) { + onItemLongClickListener.onLongClickCancel(this); + } + break; + case MotionEvent.ACTION_CANCEL: + handler.removeCallbacks(longPressRunnable); + handler.removeCallbacks(longPressStartRunnable); + break; + } + final boolean dispatchTouchEvent = super.dispatchTouchEvent(ev); + if (ev.getAction() == MotionEvent.ACTION_DOWN && !dispatchTouchEvent) { + return true; + } + return dispatchTouchEvent; + } + + public interface OnItemLongClickListener { + void onLongClickStart(View view); + + void onLongClickCancel(View view); + + void onLongClick(View view, float x, float y); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/PopupDialog.java b/app/src/main/java/awais/instagrabber/customviews/PopupDialog.java deleted file mode 100644 index 6e328dc2..00000000 --- a/app/src/main/java/awais/instagrabber/customviews/PopupDialog.java +++ /dev/null @@ -1,55 +0,0 @@ -package awais.instagrabber.customviews; - -import android.app.Dialog; -import android.content.Context; -import android.graphics.drawable.Drawable; -import android.os.IBinder; -import android.view.Gravity; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; - -import awais.instagrabber.utils.Utils; - -/** - * https://stackoverflow.com/a/15766097/1436766 - */ -public class PopupDialog extends Dialog { - private final Context context; - - public PopupDialog(Context context) { - super(context); - this.context = context; - requestWindowFeature(Window.FEATURE_NO_TITLE); - } - - public void showAtLocation(final IBinder token, final int gravity, int x, int y) { - final Window window = getWindow(); - if (window == null) return; - WindowManager.LayoutParams layoutParams = window.getAttributes(); - layoutParams.gravity = gravity; - layoutParams.x = x; - layoutParams.y = y; - // layoutParams.token = token; - show(); - } - - public void showAsDropDown(View view) { - float density = Utils.displayMetrics.density; - final Window window = getWindow(); - if (window == null) return; - WindowManager.LayoutParams layoutParams = window.getAttributes(); - int[] location = new int[2]; - view.getLocationInWindow(location); - layoutParams.gravity = Gravity.TOP | Gravity.START; - layoutParams.x = location[0] + (int) (view.getWidth() / density); - layoutParams.y = location[1] + (int) (view.getHeight() / density); - show(); - } - - public void setBackgroundDrawable(final Drawable drawable) { - final Window window = getWindow(); - if (window == null) return; - window.setBackgroundDrawable(drawable); - } -} diff --git a/app/src/main/java/awais/instagrabber/customviews/emoji/ReactionsManager.java b/app/src/main/java/awais/instagrabber/customviews/emoji/ReactionsManager.java new file mode 100644 index 00000000..0e812528 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/emoji/ReactionsManager.java @@ -0,0 +1,76 @@ +package awais.instagrabber.customviews.emoji; + +import android.util.Log; + +import com.google.common.collect.ImmutableList; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import awais.instagrabber.utils.AppExecutors; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; +import awais.instagrabber.utils.emoji.EmojiParser; + +import static awais.instagrabber.utils.Constants.PREF_REACTIONS; + +public class ReactionsManager { + private static final String TAG = ReactionsManager.class.getSimpleName(); + private static final Object LOCK = new Object(); + + private final AppExecutors appExecutors = AppExecutors.getInstance(); + private final List reactions = new ArrayList<>(); + + private static ReactionsManager instance; + + public static ReactionsManager getInstance() { + if (instance == null) { + synchronized (LOCK) { + if (instance == null) { + instance = new ReactionsManager(); + } + } + } + return instance; + } + + private ReactionsManager() { + String reactionsJson = Utils.settingsHelper.getString(PREF_REACTIONS); + if (TextUtils.isEmpty(reactionsJson)) { + final ImmutableList list = ImmutableList.of("❤️", "\uD83D\uDE02", "\uD83D\uDE2E", "\uD83D\uDE22", "\uD83D\uDE21", "\uD83D\uDC4D"); + reactionsJson = new JSONArray(list).toString(); + } + final EmojiParser emojiParser = EmojiParser.getInstance(); + final Map allEmojis = emojiParser.getAllEmojis(); + try { + final JSONArray reactionsJsonArray = new JSONArray(reactionsJson); + for (int i = 0; i < reactionsJsonArray.length(); i++) { + final String emojiUnicode = reactionsJsonArray.optString(i); + if (emojiUnicode == null) continue; + final Emoji emoji = allEmojis.get(emojiUnicode); + if (emoji == null) continue; + reactions.add(emoji); + } + } catch (JSONException e) { + Log.e(TAG, "ReactionsManager: ", e); + } + } + + public List getReactions() { + return reactions; + } + + // public void setVariant(final String parent, final String variant) { + // if (parent == null || variant == null) return; + // selectedVariantMap.put(parent, variant); + // appExecutors.tasksThread().execute(() -> { + // final JSONObject jsonObject = new JSONObject(selectedVariantMap); + // final String json = jsonObject.toString(); + // Utils.settingsHelper.putString(PREF_EMOJI_VARIANTS, json); + // }); + // } +} 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 2328098b..2b062b8d 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -57,11 +57,14 @@ import awais.instagrabber.R; import awais.instagrabber.activities.CameraActivity; import awais.instagrabber.activities.MainActivity; import awais.instagrabber.adapters.DirectItemsAdapter; +import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback; +import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemLongClickListener; import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemOrHeader; import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder; 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.helpers.HeaderItemDecoration; import awais.instagrabber.customviews.helpers.HeightProvider; import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge; @@ -133,7 +136,7 @@ public class DirectMessageThreadFragment extends Fragment { setMicToSendIcon(); } }; - private final DirectItemsAdapter.DirectItemCallback directItemCallback = new DirectItemsAdapter.DirectItemCallback() { + private final DirectItemCallback directItemCallback = new DirectItemCallback() { @Override public void onHashtagClick(final String hashtag) { final NavDirections action = DirectMessageThreadFragmentDirections.actionGlobalHashTagFragment(hashtag); @@ -188,6 +191,18 @@ public class DirectMessageThreadFragment extends Fragment { Log.e(TAG, "onStoryClick: ", e); } } + + @Override + public void onReaction(final DirectItem item, final Emoji emoji) { + if (item == null) return; + final LiveData> resourceLiveData = viewModel.sendReaction(item, emoji); + if (resourceLiveData != null) { + resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData)); + } + } + }; + private final DirectItemLongClickListener directItemLongClickListener = position -> { + // viewModel.setSelectedPosition(position); }; @Override @@ -226,6 +241,7 @@ public class DirectMessageThreadFragment extends Fragment { init(); binding.send.post(() -> initialSendX = binding.send.getX()); shouldRefresh = false; + setObservers(); } @Override @@ -309,7 +325,6 @@ public class DirectMessageThreadFragment extends Fragment { binding.input.post(this::showKeyboard); wasKbShowing = false; } - setObservers(); if (initialSendX != 0) { binding.send.setX(initialSendX); } @@ -354,7 +369,6 @@ public class DirectMessageThreadFragment extends Fragment { setupList(); root.post(this::setupInput); root.post(this::getInitialData); - setObservers(); } private void getInitialData() { @@ -578,7 +592,7 @@ public class DirectMessageThreadFragment extends Fragment { itemsAdapter.setThread(thread); return; } - itemsAdapter = new DirectItemsAdapter(currentUser, thread, directItemCallback); + itemsAdapter = new DirectItemsAdapter(currentUser, thread, directItemCallback, directItemLongClickListener); itemsAdapter.setHasStableIds(true); itemsAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); binding.chats.setAdapter(itemsAdapter); @@ -694,7 +708,8 @@ public class DirectMessageThreadFragment extends Fragment { binding.send.setOnRecordClickListener(v -> { final Editable text = binding.input.getText(); if (TextUtils.isEmpty(text)) return; - viewModel.sendText(text.toString()).observe(getViewLifecycleOwner(), this::handleSentMessage); + final LiveData> resourceLiveData = viewModel.sendText(text.toString()); + resourceLiveData.observe(getViewLifecycleOwner(), resource -> handleSentMessage(resourceLiveData)); binding.input.setText(""); }); binding.send.setOnRecordLongClickListener(v -> { @@ -754,16 +769,21 @@ public class DirectMessageThreadFragment extends Fragment { navController.navigate(navDirections); } - private void handleSentMessage(@NonNull final Resource resource) { + private void handleSentMessage(final LiveData> resourceLiveData) { + final Resource resource = resourceLiveData.getValue(); + if (resource == null) return; final Resource.Status status = resource.status; switch (status) { case SUCCESS: + resourceLiveData.removeObservers(getViewLifecycleOwner()); + break; case LOADING: break; case ERROR: if (resource.message != null) { Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); } + resourceLiveData.removeObservers(getViewLifecycleOwner()); break; } } @@ -1093,6 +1113,10 @@ public class DirectMessageThreadFragment extends Fragment { animatorSet.start(); } + private void showLongClickOptions(final View itemView) { + + } + public static class ItemsAdapterDataMerger extends MediatorLiveData> { private User user; private DirectThread thread; diff --git a/app/src/main/java/awais/instagrabber/repositories/requests/directmessages/ReactionBroadcastOptions.java b/app/src/main/java/awais/instagrabber/repositories/requests/directmessages/ReactionBroadcastOptions.java index 32388343..ccfc9795 100644 --- a/app/src/main/java/awais/instagrabber/repositories/requests/directmessages/ReactionBroadcastOptions.java +++ b/app/src/main/java/awais/instagrabber/repositories/requests/directmessages/ReactionBroadcastOptions.java @@ -4,17 +4,21 @@ import java.util.HashMap; import java.util.Map; import awais.instagrabber.models.enums.BroadcastItemType; +import awais.instagrabber.utils.TextUtils; public class ReactionBroadcastOptions extends BroadcastOptions { private final String itemId; + private final String emoji; private final boolean delete; public ReactionBroadcastOptions(final String clientContext, final ThreadIdOrUserIds threadIdOrUserIds, final String itemId, + final String emoji, final boolean delete) { super(clientContext, threadIdOrUserIds, BroadcastItemType.REACTION); this.itemId = itemId; + this.emoji = emoji; this.delete = delete; } @@ -24,6 +28,9 @@ public class ReactionBroadcastOptions extends BroadcastOptions { form.put("item_id", itemId); form.put("reaction_status", delete ? "deleted" : "created"); form.put("reaction_type", "like"); + if (!TextUtils.isEmpty(emoji)) { + form.put("emoji", emoji); + } return form; } } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java index 1b2e9292..366aa82c 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItem.java @@ -32,7 +32,7 @@ public class DirectItem implements Cloneable { private final DirectItemFelixShare felixShare; private final DirectItemVisualMedia visualMedia; private final DirectItemAnimatedMedia animatedMedia; - private final DirectItemReactions reactions; + private DirectItemReactions reactions; private final DirectItem repliedToMessage; private final DirectItemVoiceMedia voiceMedia; private final Location location; @@ -218,6 +218,10 @@ public class DirectItem implements Cloneable { isPending = pending; } + public void setReactions(final DirectItemReactions reactions) { + this.reactions = reactions; + } + @NonNull @Override public Object clone() throws CloneNotSupportedException { 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 dbcd4470..84d2af0d 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 @@ -1,5 +1,7 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.util.Objects; + public class DirectItemEmojiReaction { private final long senderId; private final long timestamp; @@ -28,4 +30,20 @@ public class DirectItemEmojiReaction { public String getSuperReactType() { return superReactType; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemEmojiReaction that = (DirectItemEmojiReaction) o; + return senderId == that.senderId && + timestamp == that.timestamp && + Objects.equals(emoji, that.emoji) && + Objects.equals(superReactType, that.superReactType); + } + + @Override + public int hashCode() { + return Objects.hash(senderId, timestamp, emoji, 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 1fd1405f..ddab4d22 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 @@ -1,10 +1,13 @@ package awais.instagrabber.repositories.responses.directmessages; -import java.util.List; +import androidx.annotation.NonNull; -public class DirectItemReactions { - private final List emojis; - private final List likes; +import java.util.List; +import java.util.Objects; + +public class DirectItemReactions implements Cloneable { + private List emojis; + private List likes; public DirectItemReactions(final List emojis, final List likes) { @@ -19,4 +22,32 @@ public class DirectItemReactions { public List getLikes() { return likes; } + + public void setLikes(final List likes) { + this.likes = likes; + } + + public void setEmojis(final List emojis) { + this.emojis = emojis; + } + + @NonNull + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final DirectItemReactions that = (DirectItemReactions) o; + return Objects.equals(emojis, that.emojis) && + Objects.equals(likes, that.likes); + } + + @Override + public int hashCode() { + return Objects.hash(emojis, likes); + } } diff --git a/app/src/main/java/awais/instagrabber/utils/Constants.java b/app/src/main/java/awais/instagrabber/utils/Constants.java index 46c682c1..7dcdf37b 100644 --- a/app/src/main/java/awais/instagrabber/utils/Constants.java +++ b/app/src/main/java/awais/instagrabber/utils/Constants.java @@ -95,4 +95,5 @@ public final class Constants { public static final String PREF_TAGGED_POSTS_LAYOUT = "tagged_posts_layout"; public static final String PREF_SAVED_POSTS_LAYOUT = "saved_posts_layout"; public static final String PREF_EMOJI_VARIANTS = "emoji_variants"; + public static final String PREF_REACTIONS = "reactions"; } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java index da393e61..204f81cd 100755 --- a/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java +++ b/app/src/main/java/awais/instagrabber/utils/SettingsHelper.java @@ -34,6 +34,7 @@ import static awais.instagrabber.utils.Constants.PREF_LIKED_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREF_LOCATION_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREF_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREF_PROFILE_POSTS_LAYOUT; +import static awais.instagrabber.utils.Constants.PREF_REACTIONS; import static awais.instagrabber.utils.Constants.PREF_SAVED_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREF_TAGGED_POSTS_LAYOUT; import static awais.instagrabber.utils.Constants.PREF_TOPIC_POSTS_LAYOUT; @@ -123,7 +124,7 @@ public final class SettingsHelper { {APP_LANGUAGE, APP_THEME, COOKIE, FOLDER_PATH, DATE_TIME_FORMAT, DATE_TIME_SELECTION, CUSTOM_DATE_TIME_FORMAT, DEVICE_UUID, SKIPPED_VERSION, DEFAULT_TAB, PREF_DARK_THEME, PREF_LIGHT_THEME, PREF_POSTS_LAYOUT, PREF_PROFILE_POSTS_LAYOUT, PREF_TOPIC_POSTS_LAYOUT, PREF_HASHTAG_POSTS_LAYOUT, PREF_LOCATION_POSTS_LAYOUT, - PREF_LIKED_POSTS_LAYOUT, PREF_TAGGED_POSTS_LAYOUT, PREF_SAVED_POSTS_LAYOUT, STORY_SORT, PREF_EMOJI_VARIANTS}) + PREF_LIKED_POSTS_LAYOUT, PREF_TAGGED_POSTS_LAYOUT, PREF_SAVED_POSTS_LAYOUT, STORY_SORT, PREF_EMOJI_VARIANTS, PREF_REACTIONS}) public @interface StringSettings {} @StringDef({DOWNLOAD_USER_FOLDER, FOLDER_SAVE_TO, AUTOPLAY_VIDEOS, SHOW_QUICK_ACCESS_DIALOG, MUTED_VIDEOS, 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 8bf27039..5018cc8a 100644 --- a/app/src/main/java/awais/instagrabber/utils/emoji/EmojiParser.java +++ b/app/src/main/java/awais/instagrabber/utils/emoji/EmojiParser.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Collection; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -51,6 +52,7 @@ public final class EmojiParser { private static final UnicodeSet SKIN_TONE_MODIFIERS = new UnicodeSet("[🏻-🏿]").freeze(); private static final String SKIN_TONE_PATTERN = SKIN_TONE_MODIFIERS.toPattern(true); private static final Map CATEGORY_MAP = new LinkedHashMap<>(); + private static final Map ALL_EMOJIS = new HashMap<>(); // private final UnicodeMap emojiToMajorCategory = new UnicodeMap<>(); // private final UnicodeMap emojiToMinorCategory = new UnicodeMap<>(); @@ -201,6 +203,7 @@ public final class EmojiParser { spacePos = comment.indexOf(' ', spacePos + 1); // get second space final String name = comment.substring(spacePos + 1).trim(); final Emoji emoji = new Emoji(original, name); + ALL_EMOJIS.put(original, emoji); String minimal = original.replace(EMOJI_VARIANT, ""); //noinspection deprecation boolean singleton = CharSequences.getSingleCodePoint(minimal) != Integer.MAX_VALUE; @@ -263,6 +266,10 @@ public final class EmojiParser { return categories; } + public Map getAllEmojis() { + return ALL_EMOJIS; + } + // public String getMinorCategory(String emoji) { // String minorCat = emojiToMinorCategory.get(emoji); // if (minorCat == null) { diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java index cfb1858e..14742b99 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java @@ -30,12 +30,15 @@ import java.util.Locale; import java.util.UUID; import java.util.stream.Collectors; +import awais.instagrabber.customviews.emoji.Emoji; import awais.instagrabber.models.Resource; import awais.instagrabber.models.UploadVideoOptions; import awais.instagrabber.repositories.requests.UploadFinishOptions; import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions.ThreadIdOrUserIds; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.directmessages.DirectItem; +import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction; +import awais.instagrabber.repositories.responses.directmessages.DirectItemReactions; import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponseMessageMetadata; @@ -74,6 +77,7 @@ public class DirectThreadViewModel extends AndroidViewModel { private final MutableLiveData threadTitle = new MutableLiveData<>(""); private final MutableLiveData fetching = new MutableLiveData<>(false); private final MutableLiveData> users = new MutableLiveData<>(new ArrayList<>()); + private final DirectMessagesService service; private final ContentResolver contentResolver; private final MediaService mediaService; @@ -154,6 +158,74 @@ public class DirectThreadViewModel extends AndroidViewModel { this.items.postValue(list); } + private void addReaction(final DirectItem item, final Emoji emoji) { + if (item == null || emoji == null || currentUser == null) return; + final boolean isLike = emoji.getUnicode().equals("❤️"); + DirectItemReactions reactions = item.getReactions(); + if (reactions == null) { + reactions = new DirectItemReactions(null, null); + } else { + try { + reactions = (DirectItemReactions) reactions.clone(); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "addReaction: ", e); + return; + } + } + if (isLike) { + final List likes = addEmoji(reactions.getLikes(), null, false); + reactions.setLikes(likes); + } + final List emojis = addEmoji(reactions.getEmojis(), emoji.getUnicode(), true); + reactions.setEmojis(emojis); + List list = this.items.getValue(); + list = list == null ? new LinkedList<>() : new LinkedList<>(list); + int index = -1; + for (int i = 0; i < list.size(); i++) { + final DirectItem directItem = list.get(i); + if (directItem.getItemId().equals(item.getItemId())) { + index = i; + break; + } + } + if (index >= 0) { + try { + final DirectItem clone = (DirectItem) list.get(index).clone(); + clone.setReactions(reactions); + list.set(index, clone); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "addReaction: error cloning", e); + } + } + this.items.postValue(list); + } + + private List addEmoji(final List reactionList, + final String emoji, + final boolean shouldReplaceIfAlreadyReacted) { + final List temp = reactionList == null ? new ArrayList<>() : new ArrayList<>(reactionList); + int index = -1; + for (int i = 0; i < temp.size(); i++) { + final DirectItemEmojiReaction directItemEmojiReaction = temp.get(i); + if (directItemEmojiReaction.getSenderId() == currentUser.getPk()) { + index = i; + break; + } + } + final DirectItemEmojiReaction reaction = new DirectItemEmojiReaction( + currentUser.getPk(), + System.currentTimeMillis() * 1000, + emoji, + "none" + ); + if (index < 0) { + temp.add(reaction); + } else if (shouldReplaceIfAlreadyReacted) { + temp.set(index, reaction); + } + return temp; + } + private void updateItemSent(final String clientContext, final long timestamp) { if (clientContext == null) return; List list = this.items.getValue(); @@ -551,6 +623,48 @@ public class DirectThreadViewModel extends AndroidViewModel { }); } + public LiveData> sendReaction(final DirectItem item, final Emoji emoji) { + final MutableLiveData> data = new MutableLiveData<>(); + final Long userId = handleCurrentUser(data); + if (userId == null) return data; + final String clientContext = UUID.randomUUID().toString(); + // Log.d(TAG, "sendText: sending: itemId: " + directItem.getItemId()); + data.postValue(Resource.loading(item)); + addReaction(item, emoji); + String emojiUnicode = null; + if (!emoji.getUnicode().equals("❤️")) { + emojiUnicode = emoji.getUnicode(); + } + final Call request = service.broadcastReaction( + clientContext, threadIdOrUserIds, item.getItemId(), emojiUnicode, false); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (!response.isSuccessful()) { + if (response.errorBody() != null) { + handleErrorBody(call, response, data, item); + return; + } + data.postValue(Resource.error("request was not successful and response error body was null", item)); + return; + } + final DirectThreadBroadcastResponse body = response.body(); + if (body == null) { + data.postValue(Resource.error("Response is null!", item)); + } + // otherwise nothing to do? maybe update the timestamp in the emoji? + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + data.postValue(Resource.error(t.getMessage(), item)); + Log.e(TAG, "enqueueRequest: onFailure: ", t); + } + }); + return data; + } + public void setCurrentUser(final User currentUser) { this.currentUser = currentUser; } diff --git a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java index f7a2c9df..89f74aa3 100644 --- a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java +++ b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java @@ -19,6 +19,7 @@ import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions.ThreadIdOrUserIds; import awais.instagrabber.repositories.requests.directmessages.LinkBroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.PhotoBroadcastOptions; +import awais.instagrabber.repositories.requests.directmessages.ReactionBroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.StoryReplyBroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.TextBroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.VideoBroadcastOptions; @@ -158,6 +159,15 @@ public class DirectMessagesService extends BaseService { return broadcast(new StoryReplyBroadcastOptions(UUID.randomUUID().toString(), threadIdOrUserIds, text, mediaId, reelId)); } + public Call broadcastReaction(final String clientContext, + final ThreadIdOrUserIds threadIdOrUserIds, + final String itemId, + final String emoji, + final boolean delete) { + return broadcast(new ReactionBroadcastOptions(clientContext, threadIdOrUserIds, itemId, emoji, delete)); + } + + private Call broadcast(@NonNull final BroadcastOptions broadcastOptions) { if (TextUtils.isEmpty(broadcastOptions.getClientContext())) { throw new IllegalArgumentException("Broadcast requires a valid client context value"); diff --git a/app/src/main/res/drawable/bg_rounded_corner.xml b/app/src/main/res/drawable/bg_rounded_corner.xml new file mode 100644 index 00000000..66d0a5ae --- /dev/null +++ b/app/src/main/res/drawable/bg_rounded_corner.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml index 0553ae30..24877ee9 100755 --- a/app/src/main/res/drawable/ic_add.xml +++ b/app/src/main/res/drawable/ic_add.xml @@ -1,10 +1,10 @@ - + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + diff --git a/app/src/main/res/layout/fragment_direct_messages_thread.xml b/app/src/main/res/layout/fragment_direct_messages_thread.xml index 530c644e..b122f24b 100644 --- a/app/src/main/res/layout/fragment_direct_messages_thread.xml +++ b/app/src/main/res/layout/fragment_direct_messages_thread.xml @@ -143,4 +143,14 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_pref_divider.xml b/app/src/main/res/layout/item_pref_divider.xml index b0ec9b3d..c5d3c97a 100644 --- a/app/src/main/res/layout/item_pref_divider.xml +++ b/app/src/main/res/layout/item_pref_divider.xml @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/layout_direct_item_options.xml b/app/src/main/res/layout/layout_direct_item_options.xml new file mode 100644 index 00000000..98ec0a1d --- /dev/null +++ b/app/src/main/res/layout/layout_direct_item_options.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_dm_base.xml b/app/src/main/res/layout/layout_dm_base.xml index f6ff6c7d..0207ebb8 100644 --- a/app/src/main/res/layout/layout_dm_base.xml +++ b/app/src/main/res/layout/layout_dm_base.xml @@ -1,5 +1,5 @@ - + android:clipToPadding="false" + android:paddingStart="4dp" + android:paddingTop="4dp" + android:paddingEnd="4dp" + tools:layout_gravity="end" + tools:paddingBottom="@dimen/dm_reaction_adjust_margin"> + + android:layout_height="wrap_content" + tools:layout_height="200dp" + tools:layout_width="100dp" /> @@ -166,24 +173,49 @@ - + app:layout_constraintTop_toBottomOf="parent" + app:layout_constraintWidth_max="wrap" + tools:text="😀" + tools:visibility="visible" /> - - + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_dm_media_share.xml b/app/src/main/res/layout/layout_dm_media_share.xml index 03340189..fcc74942 100644 --- a/app/src/main/res/layout/layout_dm_media_share.xml +++ b/app/src/main/res/layout/layout_dm_media_share.xml @@ -3,9 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/media_share_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_width="wrap_content" + android:layout_height="wrap_content"> 80dp 48dp 4dp + 22dp + 6dp + -12dp 32dp 200dp + + 40dp + 8dp + 48dp + 4dp + + 1dp \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 00000000..d4069f8d --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c3da77d4..677ad0c0 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -396,4 +396,5 @@ Remove as Admin Edit was unsuccessful Message + Reply