mirror of
				https://github.com/KokaKiwi/BarInsta
				synced 2025-10-31 03:25:34 +00:00 
			
		
		
		
	Add like/reaction (WIP)
This commit is contained in:
		
							parent
							
								
									02a1a4a5f5
								
							
						
					
					
						commit
						cf62d88531
					
				| @ -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<RecyclerView.ViewHolder> { | ||||
|     private static final String TAG = DirectItemsAdapter.class.getSimpleName(); | ||||
| 
 | ||||
|     private final User currentUser; | ||||
|     private List<DirectItem> items; | ||||
|     private DirectThread thread; | ||||
|     private DirectItemViewHolder selectedViewHolder; | ||||
| 
 | ||||
|     private final User currentUser; | ||||
|     private final DirectItemCallback callback; | ||||
|     private final AsyncListDiffer<DirectItemOrHeader> differ; | ||||
|     private List<DirectItem> items; | ||||
|     private final DirectItemInternalLongClickListener longClickListener; | ||||
| 
 | ||||
|     private static final DiffUtil.ItemCallback<DirectItemOrHeader> diffCallback = new DiffUtil.ItemCallback<DirectItemOrHeader>() { | ||||
|         @Override | ||||
| @ -96,21 +101,30 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView. | ||||
|             if (bothHeaders) { | ||||
|                 return oldItem.date.equals(newItem.date); | ||||
|             } | ||||
|             return oldItem.item.getTimestamp() == newItem.item.getTimestamp() | ||||
|                     && oldItem.item.isPending() == newItem.item.isPending(); // todo need to be more specific | ||||
|             final boolean timestampEqual = oldItem.item.getTimestamp() == newItem.item.getTimestamp(); | ||||
|             final boolean bothPending = oldItem.item.isPending() == newItem.item.isPending(); | ||||
|             final boolean reactionSame = Objects.equals(oldItem.item.getReactions(), newItem.item.getReactions()); | ||||
|             return timestampEqual && bothPending && reactionSame; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     public DirectItemsAdapter(@NonNull final User currentUser, | ||||
|                               @NonNull final DirectThread thread, | ||||
|                               @NonNull final DirectItemCallback callback) { | ||||
|                               @NonNull final DirectItemCallback callback, | ||||
|                               @NonNull final DirectItemLongClickListener itemLongClickListener) { | ||||
|         this.currentUser = currentUser; | ||||
|         this.thread = thread; | ||||
|         this.callback = callback; | ||||
|         differ = new AsyncListDiffer<>(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<RecyclerView. | ||||
|         } | ||||
|         final LayoutDmBaseBinding baseBinding = LayoutDmBaseBinding.inflate(layoutInflater, parent, false); | ||||
|         final DirectItemType directItemType = DirectItemType.valueOf(type); | ||||
|         return getItemViewHolder(layoutInflater, baseBinding, directItemType); | ||||
|         final DirectItemViewHolder itemViewHolder = getItemViewHolder(layoutInflater, baseBinding, directItemType); | ||||
|         itemViewHolder.setLongClickListener(longClickListener); | ||||
|         return itemViewHolder; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
| @ -205,7 +221,7 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView. | ||||
|             return; | ||||
|         } | ||||
|         if (thread == null) return; | ||||
|         ((DirectItemViewHolder) holder).bind(itemOrHeader.item); | ||||
|         ((DirectItemViewHolder) holder).bind(position, itemOrHeader.item); | ||||
|     } | ||||
| 
 | ||||
|     protected DirectItemOrHeader getItem(int position) { | ||||
| @ -372,5 +388,15 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView. | ||||
|         void onMediaClick(Media media); | ||||
| 
 | ||||
|         void onStoryClick(DirectItemStoryShare storyShare); | ||||
| 
 | ||||
|         void onReaction(final DirectItem item, Emoji emoji); | ||||
|     } | ||||
| 
 | ||||
|     public interface DirectItemInternalLongClickListener { | ||||
|         void onLongClick(int position, DirectItemViewHolder viewHolder); | ||||
|     } | ||||
| 
 | ||||
|     public interface DirectItemLongClickListener { | ||||
|         void onLongClick(int position); | ||||
|     } | ||||
| } | ||||
| @ -176,14 +176,20 @@ public final class DirectInboxItemViewHolder extends RecyclerView.ViewHolder { | ||||
|                     subtitle = getMediaSpecificSubtitle(username, mediaType); | ||||
|                     break; | ||||
|                 } | ||||
|                 case STORY_SHARE: | ||||
|                     String format = "%s shared a story by @%s"; | ||||
|                     if (item.getStoryShare().getReelType().equals("highlight_reel")) { | ||||
|                         format = "%s shared a story highlight by @%s"; | ||||
|                 case STORY_SHARE: { | ||||
|                     final String reelType = item.getStoryShare().getReelType(); | ||||
|                     if (reelType == null) { | ||||
|                         subtitle = item.getStoryShare().getTitle(); | ||||
|                     } else { | ||||
|                         String format = "%s shared a story by @%s"; | ||||
|                         if (reelType.equals("highlight_reel")) { | ||||
|                             format = "%s shared a story highlight by @%s"; | ||||
|                         } | ||||
|                         subtitle = String.format(format, username != null ? username : "", | ||||
|                                                  item.getStoryShare().getMedia().getUser().getUsername()); | ||||
|                     } | ||||
|                     subtitle = String.format(format, username != null ? username : "", | ||||
|                                              item.getStoryShare().getMedia().getUser().getUsername()); | ||||
|                     break; | ||||
|                 } | ||||
|                 case VOICE_MEDIA: | ||||
|                     subtitle = String.format("%s sent a voice message", username != null ? username : ""); | ||||
|                     break; | ||||
|  | ||||
| @ -70,6 +70,7 @@ public class DirectItemLinkViewHolder extends DirectItemViewHolder { | ||||
|         setupRamboTextListeners(binding.text); | ||||
|         final View.OnClickListener onClickListener = v -> 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); | ||||
|  | ||||
| @ -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<Integer, Integer> 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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<Long> 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<User> users) { | ||||
|         final DirectItemReactions reactions = item.getReactions(); | ||||
|         final List<DirectItemEmojiReaction> 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<DirectUser> 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<DirectItemEmojiReaction> 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<DirectUser> 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<DirectItemContextMenu.MenuItem> 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 | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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. | ||||
|  * <p> | ||||
|  * 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); | ||||
|     } | ||||
| } | ||||
| @ -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 { | ||||
|  | ||||
| @ -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<MenuItem> 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<MenuItem> 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<View, View> 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<View, View> addReactions(final LayoutInflater layoutInflater, final ConstraintLayout container) { | ||||
|         final List<Emoji> 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); | ||||
|     //         } | ||||
|     //     }); | ||||
|     // } | ||||
| } | ||||
| 
 | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
| @ -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<Emoji> 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<String> 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<String, Emoji> 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<Emoji> 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); | ||||
|     //     }); | ||||
|     // } | ||||
| } | ||||
| @ -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<Resource<DirectItem>> 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<Resource<DirectItem>> 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<DirectItem> resource) { | ||||
|     private void handleSentMessage(final LiveData<Resource<DirectItem>> resourceLiveData) { | ||||
|         final Resource<DirectItem> 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<Pair<User, DirectThread>> { | ||||
|         private User user; | ||||
|         private DirectThread thread; | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,10 +1,13 @@ | ||||
| package awais.instagrabber.repositories.responses.directmessages; | ||||
| 
 | ||||
| import java.util.List; | ||||
| import androidx.annotation.NonNull; | ||||
| 
 | ||||
| public class DirectItemReactions { | ||||
|     private final List<DirectItemEmojiReaction> emojis; | ||||
|     private final List<DirectItemEmojiReaction> likes; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
| 
 | ||||
| public class DirectItemReactions implements Cloneable { | ||||
|     private List<DirectItemEmojiReaction> emojis; | ||||
|     private List<DirectItemEmojiReaction> likes; | ||||
| 
 | ||||
|     public DirectItemReactions(final List<DirectItemEmojiReaction> emojis, | ||||
|                                final List<DirectItemEmojiReaction> likes) { | ||||
| @ -19,4 +22,32 @@ public class DirectItemReactions { | ||||
|     public List<DirectItemEmojiReaction> getLikes() { | ||||
|         return likes; | ||||
|     } | ||||
| 
 | ||||
|     public void setLikes(final List<DirectItemEmojiReaction> likes) { | ||||
|         this.likes = likes; | ||||
|     } | ||||
| 
 | ||||
|     public void setEmojis(final List<DirectItemEmojiReaction> 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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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"; | ||||
| } | ||||
| @ -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, | ||||
|  | ||||
| @ -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<EmojiCategoryType, EmojiCategory> CATEGORY_MAP = new LinkedHashMap<>(); | ||||
|     private static final Map<String, Emoji> ALL_EMOJIS = new HashMap<>(); | ||||
| 
 | ||||
|     // private final UnicodeMap<String> emojiToMajorCategory = new UnicodeMap<>(); | ||||
|     // private final UnicodeMap<String> 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<String, Emoji> getAllEmojis() { | ||||
|         return ALL_EMOJIS; | ||||
|     } | ||||
| 
 | ||||
|     // public String getMinorCategory(String emoji) { | ||||
|     //     String minorCat = emojiToMinorCategory.get(emoji); | ||||
|     //     if (minorCat == null) { | ||||
|  | ||||
| @ -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<String> threadTitle = new MutableLiveData<>(""); | ||||
|     private final MutableLiveData<Boolean> fetching = new MutableLiveData<>(false); | ||||
|     private final MutableLiveData<List<User>> 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<DirectItemEmojiReaction> likes = addEmoji(reactions.getLikes(), null, false); | ||||
|             reactions.setLikes(likes); | ||||
|         } | ||||
|         final List<DirectItemEmojiReaction> emojis = addEmoji(reactions.getEmojis(), emoji.getUnicode(), true); | ||||
|         reactions.setEmojis(emojis); | ||||
|         List<DirectItem> 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<DirectItemEmojiReaction> addEmoji(final List<DirectItemEmojiReaction> reactionList, | ||||
|                                                    final String emoji, | ||||
|                                                    final boolean shouldReplaceIfAlreadyReacted) { | ||||
|         final List<DirectItemEmojiReaction> 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<DirectItem> list = this.items.getValue(); | ||||
| @ -551,6 +623,48 @@ public class DirectThreadViewModel extends AndroidViewModel { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public LiveData<Resource<DirectItem>> sendReaction(final DirectItem item, final Emoji emoji) { | ||||
|         final MutableLiveData<Resource<DirectItem>> 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<DirectThreadBroadcastResponse> request = service.broadcastReaction( | ||||
|                 clientContext, threadIdOrUserIds, item.getItemId(), emojiUnicode, false); | ||||
|         request.enqueue(new Callback<DirectThreadBroadcastResponse>() { | ||||
|             @Override | ||||
|             public void onResponse(@NonNull final Call<DirectThreadBroadcastResponse> call, | ||||
|                                    @NonNull final Response<DirectThreadBroadcastResponse> 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<DirectThreadBroadcastResponse> 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; | ||||
|     } | ||||
|  | ||||
| @ -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<DirectThreadBroadcastResponse> 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<DirectThreadBroadcastResponse> broadcast(@NonNull final BroadcastOptions broadcastOptions) { | ||||
|         if (TextUtils.isEmpty(broadcastOptions.getClientContext())) { | ||||
|             throw new IllegalArgumentException("Broadcast requires a valid client context value"); | ||||
|  | ||||
							
								
								
									
										12
									
								
								app/src/main/res/drawable/bg_rounded_corner.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								app/src/main/res/drawable/bg_rounded_corner.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <shape xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:shape="rectangle"> | ||||
|     <solid android:color="?colorSurface" /> | ||||
|     <padding | ||||
|         android:bottom="4dp" | ||||
|         android:left="4dp" | ||||
|         android:right="4dp" | ||||
|         android:top="4dp" /> | ||||
| 
 | ||||
|     <corners android:radius="8dp" /> | ||||
| </shape> | ||||
| @ -1,10 +1,10 @@ | ||||
| <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:width="24dp" | ||||
|     android:height="24dp" | ||||
|     android:tint="?attr/colorControlNormal" | ||||
|     android:viewportWidth="24" | ||||
|     android:viewportHeight="24"> | ||||
|     <path | ||||
|         android:fillColor="@android:color/white" | ||||
|         android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" /> | ||||
|     android:viewportHeight="24" | ||||
|     android:tint="?attr/colorControlNormal"> | ||||
|   <path | ||||
|       android:fillColor="@android:color/white" | ||||
|       android:pathData="M18,13h-5v5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1v-5H6c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5V6c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v5h5c0.55,0 1,0.45 1,1s-0.45,1 -1,1z"/> | ||||
| </vector> | ||||
|  | ||||
| @ -143,4 +143,14 @@ | ||||
|         app:layout_constraintBottom_toBottomOf="parent" | ||||
|         app:layout_constraintEnd_toEndOf="parent" | ||||
|         app:layout_constraintStart_toStartOf="parent" /> | ||||
| 
 | ||||
|     <View | ||||
|         android:id="@+id/long_click_backdrop" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="match_parent" | ||||
|         android:clickable="true" | ||||
|         android:elevation="5dp" | ||||
|         android:focusable="true" | ||||
|         android:focusableInTouchMode="true" | ||||
|         android:visibility="gone" /> | ||||
| </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| @ -1,5 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <View xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="1dp" | ||||
|     android:layout_height="@dimen/horizontal_divider_height" | ||||
|     android:background="@drawable/pref_list_divider_material" /> | ||||
							
								
								
									
										69
									
								
								app/src/main/res/layout/layout_direct_item_options.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								app/src/main/res/layout/layout_direct_item_options.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     android:id="@+id/card" | ||||
|     android:layout_width="wrap_content" | ||||
|     android:layout_height="wrap_content" | ||||
|     app:cardCornerRadius="8dp" | ||||
|     app:cardElevation="2dp"> | ||||
| 
 | ||||
|     <androidx.constraintlayout.widget.ConstraintLayout | ||||
|         android:id="@+id/container" | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content"> | ||||
| 
 | ||||
|         <!--<androidx.appcompat.widget.AppCompatImageView--> | ||||
|         <!--    android:id="@+id/img1"--> | ||||
|         <!--    android:layout_width="48dp"--> | ||||
|         <!--    android:layout_height="0dp"--> | ||||
|         <!--    android:padding="4dp"--> | ||||
|         <!--    android:scaleType="fitCenter"--> | ||||
|         <!--    app:layout_constraintBottom_toTopOf="@id/divider"--> | ||||
|         <!--    app:layout_constraintDimensionRatio="1"--> | ||||
|         <!--    app:layout_constraintEnd_toStartOf="@id/img2"--> | ||||
|         <!--    app:layout_constraintStart_toStartOf="parent"--> | ||||
|         <!--    app:layout_constraintTop_toTopOf="parent"--> | ||||
|         <!--    tools:srcCompat="@mipmap/ic_launcher" />--> | ||||
| 
 | ||||
|         <!--<androidx.appcompat.widget.AppCompatImageView--> | ||||
|         <!--    android:id="@+id/img2"--> | ||||
|         <!--    android:layout_width="48dp"--> | ||||
|         <!--    android:layout_height="0dp"--> | ||||
|         <!--    android:padding="4dp"--> | ||||
|         <!--    android:scaleType="fitCenter"--> | ||||
|         <!--    app:layout_constraintDimensionRatio="1"--> | ||||
|         <!--    app:layout_constraintEnd_toStartOf="@id/img3"--> | ||||
|         <!--    app:layout_constraintStart_toEndOf="@id/img1"--> | ||||
|         <!--    app:layout_constraintTop_toTopOf="parent"--> | ||||
|         <!--    tools:srcCompat="@mipmap/ic_launcher" />--> | ||||
| 
 | ||||
|         <!--<awais.instagrabber.customviews.SquareImageView--> | ||||
|         <!--    android:id="@+id/img3"--> | ||||
|         <!--    android:layout_width="48dp"--> | ||||
|         <!--    android:layout_height="48dp"--> | ||||
|         <!--    android:padding="4dp"--> | ||||
|         <!--    app:layout_constraintEnd_toEndOf="parent"--> | ||||
|         <!--    app:layout_constraintStart_toEndOf="@id/img2"--> | ||||
|         <!--    app:layout_constraintTop_toTopOf="parent"--> | ||||
|         <!--    tools:srcCompat="@mipmap/ic_launcher" />--> | ||||
| 
 | ||||
|         <!--<include--> | ||||
|         <!--    android:id="@+id/divider"--> | ||||
|         <!--    layout="@layout/item_pref_divider"--> | ||||
|         <!--    android:layout_width="0dp"--> | ||||
|         <!--    android:layout_height="1dp"--> | ||||
|         <!--    app:layout_constraintEnd_toEndOf="@id/img3"--> | ||||
|         <!--    app:layout_constraintStart_toStartOf="@id/img1"--> | ||||
|         <!--    app:layout_constraintTop_toBottomOf="@id/img1" />--> | ||||
| 
 | ||||
|         <!--<androidx.appcompat.widget.AppCompatTextView--> | ||||
|         <!--    android:layout_width="0dp"--> | ||||
|         <!--    android:layout_height="wrap_content"--> | ||||
|         <!--    android:text="test"--> | ||||
|         <!--    app:layout_constraintBottom_toBottomOf="parent"--> | ||||
|         <!--    app:layout_constraintEnd_toEndOf="parent"--> | ||||
|         <!--    app:layout_constraintStart_toStartOf="parent"--> | ||||
|         <!--    app:layout_constraintTop_toBottomOf="@id/divider" />--> | ||||
| 
 | ||||
|     </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| </androidx.cardview.widget.CardView> | ||||
| @ -1,5 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| <awais.instagrabber.customviews.DirectItemFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
| @ -10,7 +10,12 @@ | ||||
|         android:layout_width="wrap_content" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_gravity="start" | ||||
|         android:padding="4dp"> | ||||
|         android:clipToPadding="false" | ||||
|         android:paddingStart="4dp" | ||||
|         android:paddingTop="4dp" | ||||
|         android:paddingEnd="4dp" | ||||
|         tools:layout_gravity="end" | ||||
|         tools:paddingBottom="@dimen/dm_reaction_adjust_margin"> | ||||
| 
 | ||||
|         <View | ||||
|             android:id="@+id/quote_line" | ||||
| @ -114,11 +119,11 @@ | ||||
|             tools:text="@string/app_name" | ||||
|             tools:visibility="visible" /> | ||||
| 
 | ||||
|         <!--app:layout_constraintBottom_toTopOf="@id/reactions"--> | ||||
|         <awais.instagrabber.customviews.ChatMessageLayout | ||||
|             android:id="@+id/chat_message_layout" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="wrap_content" | ||||
|             app:layout_constraintBottom_toTopOf="@id/reactions" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintStart_toEndOf="@id/ivProfilePic" | ||||
|             app:layout_constraintTop_toBottomOf="@id/tvUsername" | ||||
| @ -129,7 +134,9 @@ | ||||
|             <FrameLayout | ||||
|                 android:id="@+id/message" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" /> | ||||
|                 android:layout_height="wrap_content" | ||||
|                 tools:layout_height="200dp" | ||||
|                 tools:layout_width="100dp" /> | ||||
| 
 | ||||
|             <LinearLayout | ||||
|                 android:id="@+id/message_info" | ||||
| @ -158,7 +165,7 @@ | ||||
|                     android:layout_gravity="bottom" | ||||
|                     android:layout_marginStart="4dp" | ||||
|                     android:gravity="center_vertical" | ||||
|                     android:visibility="visible" | ||||
|                     android:visibility="gone" | ||||
|                     app:srcCompat="@drawable/ic_check_all_24" | ||||
|                     app:tint="@color/grey_500" | ||||
|                     tools:visibility="visible" /> | ||||
| @ -166,24 +173,49 @@ | ||||
| 
 | ||||
|         </awais.instagrabber.customviews.ChatMessageLayout> | ||||
| 
 | ||||
|         <FrameLayout | ||||
|             android:id="@+id/reactions" | ||||
|         <androidx.emoji.widget.EmojiAppCompatTextView | ||||
|             android:id="@+id/emojis" | ||||
|             android:layout_width="0dp" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_marginStart="4dp" | ||||
|             android:layout_marginBottom="4dp" | ||||
|             android:background="@drawable/bg_rounded_corner" | ||||
|             android:elevation="1dp" | ||||
|             android:maxLines="1" | ||||
|             android:padding="4dp" | ||||
|             android:textColor="?android:textColorPrimary" | ||||
|             android:textSize="18sp" | ||||
|             android:translationY="@dimen/dm_reaction_translation_y_type_1" | ||||
|             android:visibility="gone" | ||||
|             app:layout_constraintEnd_toEndOf="@id/chat_message_layout" | ||||
|             app:layout_constraintBottom_toBottomOf="parent" | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintHorizontal_bias="0" | ||||
|             app:layout_constraintStart_toStartOf="@id/chat_message_layout" | ||||
|             app:layout_constraintTop_toBottomOf="@id/chat_message_layout" | ||||
|             tools:visibility="visible"> | ||||
|             app:layout_constraintTop_toBottomOf="parent" | ||||
|             app:layout_constraintWidth_max="wrap" | ||||
|             tools:text="😀" | ||||
|             tools:visibility="visible" /> | ||||
| 
 | ||||
|             <androidx.appcompat.widget.AppCompatTextView | ||||
|                 android:id="@+id/emojis" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:padding="4dp" | ||||
|                 android:textColor="?android:textColorPrimary" | ||||
|                 tools:text="😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" /> | ||||
|         </FrameLayout> | ||||
|         <!--<FrameLayout--> | ||||
|         <!--    android:id="@+id/reactions"--> | ||||
|         <!--    android:layout_width="0dp"--> | ||||
|         <!--    android:layout_height="wrap_content"--> | ||||
|         <!--    android:background="@drawable/bg_rounded_corner"--> | ||||
|         <!--    android:elevation="1dp"--> | ||||
|         <!--    android:visibility="gone"--> | ||||
|         <!--    app:layout_constraintEnd_toEndOf="@id/chat_message_layout"--> | ||||
|         <!--    app:layout_constraintStart_toStartOf="@id/chat_message_layout"--> | ||||
|         <!--    app:layout_constraintTop_toBottomOf="@id/chat_message_layout"--> | ||||
|         <!--    tools:visibility="visible">--> | ||||
| 
 | ||||
|         <!--    <androidx.appcompat.widget.AppCompatTextView--> | ||||
|         <!--        android:id="@+id/emojis"--> | ||||
|         <!--        android:layout_width="wrap_content"--> | ||||
|         <!--        android:layout_height="wrap_content"--> | ||||
|         <!--        android:padding="4dp"--> | ||||
|         <!--        android:textColor="?android:textColorPrimary"--> | ||||
|         <!--        tools:text="😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" />--> | ||||
|         <!--</FrameLayout>--> | ||||
| 
 | ||||
|     </androidx.constraintlayout.widget.ConstraintLayout> | ||||
| </FrameLayout> | ||||
| </awais.instagrabber.customviews.DirectItemFrameLayout> | ||||
| @ -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"> | ||||
| 
 | ||||
|     <androidx.constraintlayout.widget.Barrier | ||||
|         android:id="@+id/top_barrier" | ||||
|  | ||||
| @ -32,7 +32,17 @@ | ||||
|     <dimen name="dm_message_item_margin">80dp</dimen> | ||||
|     <dimen name="dm_message_item_avatar_size">48dp</dimen> | ||||
|     <dimen name="dm_message_info_padding_small">4dp</dimen> | ||||
|     <dimen name="dm_reaction_adjust_margin">22dp</dimen> | ||||
|     <dimen name="dm_reaction_translation_y_type_1">6dp</dimen> | ||||
|     <dimen name="dm_reaction_translation_y_type_2">-12dp</dimen> | ||||
| 
 | ||||
|     <dimen name="feed_item_bottom_icon_size">32dp</dimen> | ||||
|     <dimen name="keyboard_height">200dp</dimen> | ||||
| 
 | ||||
|     <dimen name="reaction_picker_emoji_size">40dp</dimen> | ||||
|     <dimen name="reaction_picker_emoji_margin">8dp</dimen> | ||||
|     <dimen name="reaction_picker_option_height">48dp</dimen> | ||||
|     <dimen name="reaction_picker_add_padding_adjustment">4dp</dimen> | ||||
| 
 | ||||
|     <dimen name="horizontal_divider_height">1dp</dimen> | ||||
| </resources> | ||||
							
								
								
									
										5
									
								
								app/src/main/res/values/ids.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/values/ids.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|     <item name="reply" type="id" /> | ||||
|     <item name="unsend" type="id" /> | ||||
| </resources> | ||||
| @ -396,4 +396,5 @@ | ||||
|     <string name="dms_action_remove_admin">Remove as Admin</string> | ||||
|     <string name="edit_unsuccessful">Edit was unsuccessful</string> | ||||
|     <string name="message">Message</string> | ||||
|     <string name="reply">Reply</string> | ||||
| </resources> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user