From 6a163454f42ce854dc0eaec6c99d987bf22a5ed1 Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Fri, 15 Jan 2021 02:24:12 +0900 Subject: [PATCH] Allow removing like/reaction --- .../adapters/DirectItemsAdapter.java | 4 +- .../adapters/DirectMessageInboxAdapter.java | 2 +- .../adapters/DirectReactionsAdapter.java | 81 +++++++++++ .../adapters/DirectUsersAdapter.java | 2 +- .../adapters/UserSearchResultsAdapter.java | 2 +- .../DirectInboxItemViewHolder.java | 2 +- .../directmessages/DirectItemViewHolder.java | 35 +++-- .../DirectReactionViewHolder.java | 71 ++++++++++ .../DirectUserViewHolder.java | 2 +- .../customviews/ReactionEmojiTextView.java | 82 +++++++++++ .../DirectItemReactionDialogFragment.java | 121 ++++++++++++++++ .../DirectMessageThreadFragment.java | 61 +++++++- .../DirectItemEmojiReaction.java | 3 +- .../directmessages/DirectItemReactions.java | 3 +- .../instagrabber/utils/emoji/EmojiParser.java | 7 + .../viewmodels/DirectThreadViewModel.java | 134 ++++++++++++++++-- app/src/main/res/layout/layout_dm_base.xml | 30 ++-- .../main/res/layout/layout_dm_user_item.xml | 18 ++- app/src/main/res/values/dimens.xml | 2 +- app/src/main/res/values/strings.xml | 1 + 20 files changed, 613 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/adapters/DirectReactionsAdapter.java rename app/src/main/java/awais/instagrabber/adapters/viewholder/{ => directmessages}/DirectInboxItemViewHolder.java (99%) create mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectReactionViewHolder.java rename app/src/main/java/awais/instagrabber/adapters/viewholder/{ => directmessages}/DirectUserViewHolder.java (98%) create mode 100644 app/src/main/java/awais/instagrabber/customviews/ReactionEmojiTextView.java create mode 100644 app/src/main/java/awais/instagrabber/dialogs/DirectItemReactionDialogFragment.java diff --git a/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java index 5b57bf2f..5b7fc442 100644 --- a/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/DirectItemsAdapter.java @@ -389,7 +389,9 @@ public final class DirectItemsAdapter extends RecyclerView.Adapter { + + private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull final DirectItemEmojiReaction oldItem, @NonNull final DirectItemEmojiReaction newItem) { + return oldItem.getSenderId() == newItem.getSenderId(); + } + + @Override + public boolean areContentsTheSame(@NonNull final DirectItemEmojiReaction oldItem, @NonNull final DirectItemEmojiReaction newItem) { + return oldItem.getEmoji().equals(newItem.getEmoji()); + } + }; + + private final long viewerId; + private final List users; + private final String itemId; + private final OnReactionClickListener onReactionClickListener; + + public DirectReactionsAdapter(final long viewerId, + final List users, + final String itemId, + final OnReactionClickListener onReactionClickListener) { + super(DIFF_CALLBACK); + this.viewerId = viewerId; + this.users = users; + this.itemId = itemId; + this.onReactionClickListener = onReactionClickListener; + setHasStableIds(true); + } + + @NonNull + @Override + public DirectReactionViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + final LayoutDmUserItemBinding binding = LayoutDmUserItemBinding.inflate(layoutInflater, parent, false); + return new DirectReactionViewHolder(binding, viewerId, itemId, onReactionClickListener); + + } + + @Override + public void onBindViewHolder(@NonNull final DirectReactionViewHolder holder, final int position) { + final DirectItemEmojiReaction reaction = getItem(position); + if (reaction == null) return; + holder.bind(reaction, getUser(reaction.getSenderId())); + } + + @Override + public long getItemId(final int position) { + return getItem(position).getSenderId(); + } + + @Nullable + private User getUser(final long pk) { + return users.stream() + .filter(user -> user.getPk() == pk) + .findFirst() + .orElse(null); + } + + public interface OnReactionClickListener { + void onReactionClick(String itemId, DirectItemEmojiReaction reaction); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/DirectUsersAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DirectUsersAdapter.java index f0a44207..4b86c426 100644 --- a/app/src/main/java/awais/instagrabber/adapters/DirectUsersAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/DirectUsersAdapter.java @@ -14,7 +14,7 @@ import com.google.common.collect.ImmutableList; import java.util.List; import awais.instagrabber.R; -import awais.instagrabber.adapters.viewholder.DirectUserViewHolder; +import awais.instagrabber.adapters.viewholder.directmessages.DirectUserViewHolder; import awais.instagrabber.databinding.ItemFavSectionHeaderBinding; import awais.instagrabber.databinding.LayoutDmUserItemBinding; import awais.instagrabber.repositories.responses.User; diff --git a/app/src/main/java/awais/instagrabber/adapters/UserSearchResultsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/UserSearchResultsAdapter.java index e539907b..7363fc66 100644 --- a/app/src/main/java/awais/instagrabber/adapters/UserSearchResultsAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/UserSearchResultsAdapter.java @@ -12,7 +12,7 @@ import java.util.List; import java.util.Set; import awais.instagrabber.adapters.DirectUsersAdapter.OnDirectUserClickListener; -import awais.instagrabber.adapters.viewholder.DirectUserViewHolder; +import awais.instagrabber.adapters.viewholder.directmessages.DirectUserViewHolder; import awais.instagrabber.databinding.LayoutDmUserItemBinding; import awais.instagrabber.repositories.responses.User; diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/DirectInboxItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java similarity index 99% rename from app/src/main/java/awais/instagrabber/adapters/viewholder/DirectInboxItemViewHolder.java rename to app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java index ea16d0e3..9ce8ed84 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/DirectInboxItemViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectInboxItemViewHolder.java @@ -1,4 +1,4 @@ -package awais.instagrabber.adapters.viewholder; +package awais.instagrabber.adapters.viewholder.directmessages; import android.graphics.Typeface; import android.view.View; diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java index 5a5003c1..2503bcbd 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectItemViewHolder.java @@ -24,7 +24,6 @@ import androidx.transition.TransitionManager; import com.google.android.material.transition.MaterialFade; import java.util.List; -import java.util.Locale; import java.util.stream.Collectors; import awais.instagrabber.R; @@ -109,7 +108,7 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { 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(() -> bindBase(item, messageDirection, position)); itemView.post(() -> bindItem(item, messageDirection)); itemView.post(() -> setupLongClickListener(position)); // bindBase(item, messageDirection); @@ -117,7 +116,7 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { // setupLongClickListener(position); } - private void bindBase(final DirectItem item, final MessageDirection messageDirection) { + private void bindBase(final DirectItem item, final MessageDirection messageDirection, final int position) { final FrameLayout.LayoutParams containerLayoutParams = (FrameLayout.LayoutParams) binding.container.getLayoutParams(); final DirectItemType itemType = item.getItemType(); setMessageDirectionGravity(messageDirection, containerLayoutParams); @@ -134,7 +133,7 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { binding.messageInfo.setPadding(0, 0, messageInfoPaddingSmall, dmRadiusSmall); } setupReply(item, messageDirection); - setReactions(item); + setReactions(item, position); } private void setBackground(final MessageDirection messageDirection) { @@ -334,7 +333,7 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { replyInfoLayoutParams.endToStart = isIncoming ? ConstraintLayout.LayoutParams.UNSET : quoteLineId; } - private void setReactions(final DirectItem item) { + private void setReactions(final DirectItem item, final int position) { binding.getRoot().post(() -> { MaterialFade materialFade = new MaterialFade(); materialFade.addTarget(binding.emojis); @@ -343,17 +342,27 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { final List emojis = reactions != null ? reactions.getEmojis() : null; if (emojis == null || emojis.isEmpty()) { binding.container.setPadding(messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall, messageInfoPaddingSmall); - binding.emojis.setVisibility(View.GONE); + binding.reactionsWrapper.setVisibility(View.GONE); return; } - binding.emojis.setVisibility(View.VISIBLE); - binding.emojis.setTranslationY(getReactionsTranslationY()); + binding.reactionsWrapper.setVisibility(View.VISIBLE); + binding.reactionsWrapper.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); + binding.emojis.setEmojis(emojis.stream() + .map(DirectItemEmojiReaction::getEmoji) + .collect(Collectors.toList())); + // binding.emojis.setEmojis(ImmutableList.of("😣", + // "😖", + // "😫", + // "😩", + // "🥺", + // "😢", + // "😭", + // "😤", + // "😠", + // "😡", + // "🤬")); + binding.emojis.setOnClickListener(v -> callback.onReactionClick(item, position)); // final List reactedUsers = emojis.stream() // .map(DirectItemEmojiReaction::getSenderId) // .distinct() diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectReactionViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectReactionViewHolder.java new file mode 100644 index 00000000..497f7a06 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectReactionViewHolder.java @@ -0,0 +1,71 @@ +package awais.instagrabber.adapters.viewholder.directmessages; + +import android.view.View; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.DirectReactionsAdapter.OnReactionClickListener; +import awais.instagrabber.customviews.emoji.Emoji; +import awais.instagrabber.databinding.LayoutDmUserItemBinding; +import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction; +import awais.instagrabber.utils.emoji.EmojiParser; + +public class DirectReactionViewHolder extends RecyclerView.ViewHolder { + private final LayoutDmUserItemBinding binding; + private final long viewerId; + private final String itemId; + private final OnReactionClickListener onReactionClickListener; + private final EmojiParser emojiParser; + + public DirectReactionViewHolder(final LayoutDmUserItemBinding binding, + final long viewerId, + final String itemId, + final OnReactionClickListener onReactionClickListener) { + super(binding.getRoot()); + this.binding = binding; + this.viewerId = viewerId; + this.itemId = itemId; + this.onReactionClickListener = onReactionClickListener; + binding.info.setVisibility(View.GONE); + binding.secondaryImage.setVisibility(View.VISIBLE); + emojiParser = EmojiParser.getInstance(); + } + + public void bind(final DirectItemEmojiReaction reaction, + @Nullable final User user) { + itemView.setOnClickListener(v -> { + if (onReactionClickListener == null) return; + onReactionClickListener.onReactionClick(itemId, reaction); + }); + setUser(user); + setReaction(reaction); + } + + private void setReaction(final DirectItemEmojiReaction reaction) { + final Emoji emoji = emojiParser.getEmoji(reaction.getEmoji()); + if (emoji == null) { + binding.secondaryImage.setImageDrawable(null); + return; + } + binding.secondaryImage.setImageDrawable(emoji.getDrawable()); + } + + private void setUser(final User user) { + if (user == null) { + binding.fullName.setText(""); + binding.username.setText(""); + binding.profilePic.setImageURI((String) null); + return; + } + binding.fullName.setText(user.getFullName()); + if (user.getPk() == viewerId) { + binding.username.setText(R.string.tap_to_remove); + } else { + binding.username.setText(user.getUsername()); + } + binding.profilePic.setImageURI(user.getProfilePicUrl()); + } +} diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/DirectUserViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectUserViewHolder.java similarity index 98% rename from app/src/main/java/awais/instagrabber/adapters/viewholder/DirectUserViewHolder.java rename to app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectUserViewHolder.java index 709a1f60..695ae700 100644 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/DirectUserViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectUserViewHolder.java @@ -1,4 +1,4 @@ -package awais.instagrabber.adapters.viewholder; +package awais.instagrabber.adapters.viewholder.directmessages; import android.graphics.drawable.Drawable; import android.text.SpannableStringBuilder; diff --git a/app/src/main/java/awais/instagrabber/customviews/ReactionEmojiTextView.java b/app/src/main/java/awais/instagrabber/customviews/ReactionEmojiTextView.java new file mode 100644 index 00000000..2e4e7a76 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/ReactionEmojiTextView.java @@ -0,0 +1,82 @@ +package awais.instagrabber.customviews; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.emoji.widget.EmojiAppCompatTextView; + +import java.util.List; +import java.util.stream.Collectors; + +public class ReactionEmojiTextView extends EmojiAppCompatTextView { + private static final String TAG = ReactionEmojiTextView.class.getSimpleName(); + + private final SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(); + + private String count = ""; + private SpannableString ellipsisSpannable; + private String distinctEmojis; + + public ReactionEmojiTextView(final Context context) { + super(context); + init(); + } + + public ReactionEmojiTextView(final Context context, final AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ReactionEmojiTextView(final Context context, final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + ellipsisSpannable = new SpannableString(count); + } + + @SuppressLint("SetTextI18n") + public void setEmojis(@NonNull final List emojis) { + count = String.valueOf(emojis.size()); + distinctEmojis = emojis.stream() + .distinct() + .collect(Collectors.joining()); + ellipsisSpannable = new SpannableString(count); + setText(distinctEmojis + (emojis.size() > 1 ? count : "")); + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final CharSequence text = getText(); + if (text == null) return; + final int measuredWidth = getMeasuredWidth(); + float availableTextWidth = measuredWidth - getCompoundPaddingLeft() - getCompoundPaddingRight(); + CharSequence ellipsizedText = TextUtils.ellipsize(text, getPaint(), availableTextWidth, getEllipsize()); + if (!ellipsizedText.toString().equals(text.toString())) { + // If the ellipsizedText is different than the original text, this means that it didn't fit and got indeed ellipsized. + // Calculate the new availableTextWidth by taking into consideration the size of the custom ellipsis, too. + availableTextWidth = (availableTextWidth - getPaint().measureText(count)); + ellipsizedText = TextUtils.ellipsize(text, getPaint(), availableTextWidth, getEllipsize()); + final int defaultEllipsisStart = ellipsizedText.toString().indexOf(getDefaultEllipsis()); + final int defaultEllipsisEnd = defaultEllipsisStart + 1; + spannableStringBuilder.clear(); + // Update the text with the ellipsized version and replace the default ellipsis with the custom one. + final SpannableStringBuilder replace = spannableStringBuilder.append(ellipsizedText) + .replace(defaultEllipsisStart, defaultEllipsisEnd, ellipsisSpannable); + setText(replace); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + private char getDefaultEllipsis() { + return '…'; + } + +} diff --git a/app/src/main/java/awais/instagrabber/dialogs/DirectItemReactionDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/DirectItemReactionDialogFragment.java new file mode 100644 index 00000000..f8167230 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/dialogs/DirectItemReactionDialogFragment.java @@ -0,0 +1,121 @@ +package awais.instagrabber.dialogs; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.DirectReactionsAdapter; +import awais.instagrabber.adapters.DirectReactionsAdapter.OnReactionClickListener; +import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.repositories.responses.directmessages.DirectItemReactions; +import awais.instagrabber.utils.TextUtils; + +public class DirectItemReactionDialogFragment extends BottomSheetDialogFragment { + + private static final String ARG_VIEWER_ID = "viewerId"; + private static final String ARG_ITEM_ID = "itemId"; + private static final String ARG_USERS = "users"; + private static final String ARG_REACTIONS = "reactions"; + + private RecyclerView recyclerView; + private OnReactionClickListener onReactionClickListener; + + public static DirectItemReactionDialogFragment newInstance(final long viewerId, + @NonNull final ArrayList users, + @NonNull final String itemId, + @NonNull final DirectItemReactions reactions) { + Bundle args = new Bundle(); + args.putLong(ARG_VIEWER_ID, viewerId); + args.putSerializable(ARG_USERS, users); + args.putString(ARG_ITEM_ID, itemId); + args.putSerializable(ARG_REACTIONS, reactions); + DirectItemReactionDialogFragment fragment = new DirectItemReactionDialogFragment(); + fragment.setArguments(args); + return fragment; + } + + public DirectItemReactionDialogFragment() {} + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(DialogFragment.STYLE_NORMAL, R.style.ThemeOverlay_Rounded_BottomSheetDialog); + } + + @Nullable + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + final Context context = getContext(); + if (context == null) { + return null; + } + recyclerView = new RecyclerView(context); + return recyclerView; + + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + init(); + } + + @Override + public void onAttach(@NonNull final Context context) { + super.onAttach(context); + try { + onReactionClickListener = (OnReactionClickListener) getParentFragment(); + } catch (ClassCastException e) { + throw new ClassCastException("Calling fragment must implement DirectReactionsAdapter.OnReactionClickListener interface"); + } + } + + @Override + public void onStart() { + super.onStart(); + final Dialog dialog = getDialog(); + if (dialog == null) return; + final BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialog; + final View bottomSheetInternal = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet); + if (bottomSheetInternal == null) return; + bottomSheetInternal.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + bottomSheetInternal.requestLayout(); + } + + private void init() { + final Context context = getContext(); + if (context == null) return; + final Bundle arguments = getArguments(); + if (arguments == null) return; + final long viewerId = arguments.getLong(ARG_VIEWER_ID); + final Serializable usersSerializable = arguments.getSerializable(ARG_USERS); + if (!(usersSerializable instanceof ArrayList)) return; + //noinspection unchecked + final List users = (ArrayList) usersSerializable; + final Serializable reactionsSerializable = arguments.getSerializable(ARG_REACTIONS); + if (!(reactionsSerializable instanceof DirectItemReactions)) return; + final DirectItemReactions reactions = (DirectItemReactions) reactionsSerializable; + final String itemId = arguments.getString(ARG_ITEM_ID); + if (TextUtils.isEmpty(itemId)) return; + recyclerView.setLayoutManager(new LinearLayoutManager(context)); + final DirectReactionsAdapter adapter = new DirectReactionsAdapter(viewerId, users, itemId, onReactionClickListener); + recyclerView.setAdapter(adapter); + adapter.submitList(reactions.getEmojis()); + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java index 57296c2b..3d64f18c 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -50,6 +50,7 @@ import com.google.android.material.snackbar.Snackbar; import com.google.common.collect.ImmutableList; import java.io.File; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -60,6 +61,7 @@ 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.DirectReactionsAdapter; import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder; import awais.instagrabber.animations.CubicBezierInterpolator; import awais.instagrabber.customviews.RecordView; @@ -70,6 +72,7 @@ import awais.instagrabber.customviews.helpers.HeightProvider; import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge; import awais.instagrabber.customviews.helpers.TextWatcherAdapter; import awais.instagrabber.databinding.FragmentDirectMessagesThreadBinding; +import awais.instagrabber.dialogs.DirectItemReactionDialogFragment; import awais.instagrabber.dialogs.MediaPickerBottomDialogFragment; import awais.instagrabber.fragments.PostViewV2Fragment; import awais.instagrabber.models.Resource; @@ -77,6 +80,7 @@ import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.responses.Media; 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.DirectItemStoryShare; import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.utils.AppExecutors; @@ -90,7 +94,7 @@ import awais.instagrabber.viewmodels.DirectThreadViewModel; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN; -public class DirectMessageThreadFragment extends Fragment { +public class DirectMessageThreadFragment extends Fragment implements DirectReactionsAdapter.OnReactionClickListener { private static final String TAG = DirectMessageThreadFragment.class.getSimpleName(); private static final int STORAGE_PERM_REQUEST_CODE = 8020; private static final int AUDIO_RECORD_PERM_REQUEST_CODE = 1000; @@ -145,9 +149,7 @@ public class DirectMessageThreadFragment extends Fragment { @Override public void onMentionClick(final String mention) { - final Bundle bundle = new Bundle(); - bundle.putString("username", "@" + mention); - NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(R.id.action_global_profileFragment, bundle); + navigateToUser(mention); } @Override @@ -215,10 +217,17 @@ public class DirectMessageThreadFragment extends Fragment { resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData)); } } + + @Override + public void onReactionClick(final DirectItem item, final int position) { + showReactionsDialog(item); + } }; + private final DirectItemLongClickListener directItemLongClickListener = position -> { // viewModel.setSelectedPosition(position); }; + private DirectItemReactionDialogFragment reactionDialogFragment; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { @@ -1132,6 +1141,50 @@ public class DirectMessageThreadFragment extends Fragment { } + private void showReactionsDialog(final DirectItem item) { + final LiveData> users = viewModel.getUsers(); + final LiveData> leftUsers = viewModel.getLeftUsers(); + final ArrayList allUsers = new ArrayList<>(); + allUsers.add(viewModel.getCurrentUser()); + if (users != null && users.getValue() != null) { + allUsers.addAll(users.getValue()); + } + if (leftUsers != null && leftUsers.getValue() != null) { + allUsers.addAll(leftUsers.getValue()); + } + reactionDialogFragment = DirectItemReactionDialogFragment + .newInstance(viewModel.getViewerId(), + allUsers, + item.getItemId(), + item.getReactions()); + reactionDialogFragment.show(getChildFragmentManager(), "reactions_dialog"); + } + + @Override + public void onReactionClick(final String itemId, final DirectItemEmojiReaction reaction) { + if (reactionDialogFragment != null) { + reactionDialogFragment.dismiss(); + } + if (reaction == null) return; + if (reaction.getSenderId() == viewModel.getViewerId()) { + final LiveData> resourceLiveData = viewModel.sendDeleteReaction(itemId); + if (resourceLiveData != null) { + resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData)); + } + return; + } + // navigate to user + final User user = viewModel.getUser(reaction.getSenderId()); + if (user == null) return; + navigateToUser(user.getUsername()); + } + + private void navigateToUser(@NonNull final String username) { + final Bundle bundle = new Bundle(); + bundle.putString("username", "@" + username); + NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(R.id.action_global_profileFragment, bundle); + } + public static class ItemsAdapterDataMerger extends MediatorLiveData> { private User user; private DirectThread thread; diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemEmojiReaction.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemEmojiReaction.java index 84d2af0d..c0a8b124 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemEmojiReaction.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemEmojiReaction.java @@ -1,8 +1,9 @@ package awais.instagrabber.repositories.responses.directmessages; +import java.io.Serializable; import java.util.Objects; -public class DirectItemEmojiReaction { +public class DirectItemEmojiReaction implements Serializable { private final long senderId; private final long timestamp; private final String emoji; diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReactions.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReactions.java index ddab4d22..4b344e14 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReactions.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectItemReactions.java @@ -2,10 +2,11 @@ package awais.instagrabber.repositories.responses.directmessages; import androidx.annotation.NonNull; +import java.io.Serializable; import java.util.List; import java.util.Objects; -public class DirectItemReactions implements Cloneable { +public class DirectItemReactions implements Cloneable, Serializable { private List emojis; private List likes; diff --git a/app/src/main/java/awais/instagrabber/utils/emoji/EmojiParser.java b/app/src/main/java/awais/instagrabber/utils/emoji/EmojiParser.java index 5018cc8a..7cae5167 100644 --- a/app/src/main/java/awais/instagrabber/utils/emoji/EmojiParser.java +++ b/app/src/main/java/awais/instagrabber/utils/emoji/EmojiParser.java @@ -270,6 +270,13 @@ public final class EmojiParser { return ALL_EMOJIS; } + public Emoji getEmoji(final String emoji) { + if (emoji == null) { + return null; + } + return ALL_EMOJIS.get(emoji); + } + // public String getMinorCategory(String emoji) { // String minorCat = emojiToMinorCategory.get(emoji); // if (minorCat == null) { diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java index 514ec57a..3e8ba883 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java @@ -77,6 +77,7 @@ public class DirectThreadViewModel extends AndroidViewModel { private final MutableLiveData threadTitle = new MutableLiveData<>(""); private final MutableLiveData fetching = new MutableLiveData<>(false); private final MutableLiveData> users = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> leftUsers = new MutableLiveData<>(new ArrayList<>()); private final DirectMessagesService service; private final ContentResolver contentResolver; @@ -92,18 +93,19 @@ public class DirectThreadViewModel extends AndroidViewModel { private User currentUser; private Call chatsRequest; private VoiceRecorder voiceRecorder; + private final long viewerId; public DirectThreadViewModel(@NonNull final Application application) { super(application); final String cookie = settingsHelper.getString(Constants.COOKIE); - final long userId = CookieUtils.getUserIdFromCookie(cookie); + viewerId = CookieUtils.getUserIdFromCookie(cookie); final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); - if (TextUtils.isEmpty(csrfToken) || userId <= 0 || TextUtils.isEmpty(deviceUuid)) { + if (TextUtils.isEmpty(csrfToken) || viewerId <= 0 || TextUtils.isEmpty(deviceUuid)) { throw new IllegalArgumentException("User is not logged in!"); } - service = DirectMessagesService.getInstance(csrfToken, userId, deviceUuid); - mediaService = MediaService.getInstance(deviceUuid, csrfToken, userId); + service = DirectMessagesService.getInstance(csrfToken, viewerId, deviceUuid); + mediaService = MediaService.getInstance(deviceUuid, csrfToken, viewerId); contentResolver = application.getContentResolver(); recordingsDir = DirectoryUtils.getOutputMediaDirectory(application, "Recordings"); this.application = application; @@ -138,6 +140,10 @@ public class DirectThreadViewModel extends AndroidViewModel { return items; } + public long getViewerId() { + return viewerId; + } + public void setItems(final List items) { this.items.postValue(items); } @@ -219,13 +225,53 @@ public class DirectThreadViewModel extends AndroidViewModel { "none" ); if (index < 0) { - temp.add(reaction); + temp.add(0, reaction); } else if (shouldReplaceIfAlreadyReacted) { - temp.set(index, reaction); + temp.add(0, reaction); + temp.remove(index); } return temp; } + private void removeReaction(final DirectItem item) { + try { + final DirectItem itemClone = (DirectItem) item.clone(); + final DirectItemReactions reactions = itemClone.getReactions(); + final DirectItemReactions reactionsClone = (DirectItemReactions) reactions.clone(); + final List likes = reactionsClone.getLikes(); + if (likes != null) { + final List updatedLikes = likes.stream() + .filter(like -> like.getSenderId() != viewerId) + .collect(Collectors.toList()); + reactionsClone.setLikes(updatedLikes); + } + final List emojis = reactionsClone.getEmojis(); + if (emojis != null) { + final List updatedEmojis = emojis.stream() + .filter(emoji -> emoji.getSenderId() != viewerId) + .collect(Collectors.toList()); + reactionsClone.setEmojis(updatedEmojis); + } + itemClone.setReactions(reactionsClone); + List list = this.items.getValue(); + list = list == null ? new LinkedList<>() : new LinkedList<>(list); + int index = -1; + for (int i = 0; i < list.size(); i++) { + final DirectItem directItem = list.get(i); + if (directItem.getItemId().equals(item.getItemId())) { + index = i; + break; + } + } + if (index >= 0) { + list.set(index, itemClone); + } + this.items.postValue(list); + } catch (Exception e) { + Log.e(TAG, "removeReaction: ", e); + } + } + private void updateItemSent(final String clientContext, final long timestamp) { if (clientContext == null) return; List list = this.items.getValue(); @@ -259,6 +305,10 @@ public class DirectThreadViewModel extends AndroidViewModel { return users; } + public LiveData> getLeftUsers() { + return leftUsers; + } + public void fetchChats() { final Boolean isFetching = fetching.getValue(); if ((isFetching != null && isFetching) || !hasOlder) return; @@ -319,9 +369,8 @@ public class DirectThreadViewModel extends AndroidViewModel { threadTitle.postValue(thread.getThreadTitle()); cursor = thread.getOldestCursor(); hasOlder = thread.hasOlder(); - if (users.getValue() == null || users.getValue().isEmpty()) { - users.postValue(thread.getUsers()); - } + users.postValue(thread.getUsers()); + leftUsers.postValue(thread.getLeftUsers()); fetching.postValue(false); } @@ -637,6 +686,33 @@ public class DirectThreadViewModel extends AndroidViewModel { } final Call request = service.broadcastReaction( clientContext, threadIdOrUserIds, item.getItemId(), emojiUnicode, false); + handleBroadcastReactionRequest(data, item, request); + return data; + } + + public LiveData> sendDeleteReaction(final String itemId) { + final MutableLiveData> data = new MutableLiveData<>(); + final DirectItem item = getItem(itemId); + if (item == null) { + data.postValue(Resource.error("Invalid item", null)); + return data; + } + final DirectItemReactions reactions = item.getReactions(); + if (reactions == null) { + // already removed? + data.postValue(Resource.success(item)); + return data; + } + removeReaction(item); + final String clientContext = UUID.randomUUID().toString(); + final Call request = service.broadcastReaction(clientContext, threadIdOrUserIds, item.getItemId(), null, true); + handleBroadcastReactionRequest(data, item, request); + return data; + } + + private void handleBroadcastReactionRequest(final MutableLiveData> data, + final DirectItem item, + @NonNull final Call request) { request.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @@ -662,13 +738,51 @@ public class DirectThreadViewModel extends AndroidViewModel { Log.e(TAG, "enqueueRequest: onFailure: ", t); } }); - return data; + } + + @Nullable + private DirectItem getItem(final String itemId) { + if (itemId == null) return null; + final List items = this.items.getValue(); + if (items == null) return null; + return items.stream() + .filter(directItem -> directItem.getItemId().equals(itemId)) + .findFirst() + .orElse(null); + } + + public User getCurrentUser() { + return currentUser; } public void setCurrentUser(final User currentUser) { this.currentUser = currentUser; } + @Nullable + public User getUser(final long userId) { + final LiveData> users = getUsers(); + User match = null; + if (users != null && users.getValue() != null) { + final List userList = users.getValue(); + match = userList.stream() + .filter(user -> user.getPk() == userId) + .findFirst() + .orElse(null); + } + if (match == null) { + final LiveData> leftUsers = getLeftUsers(); + if (leftUsers != null && leftUsers.getValue() != null) { + final List userList = leftUsers.getValue(); + match = userList.stream() + .filter(user -> user.getPk() == userId) + .findFirst() + .orElse(null); + } + } + return match; + } + private void enqueueRequest(@NonNull final Call request, @NonNull final MutableLiveData> data, @NonNull final DirectItem directItem) { diff --git a/app/src/main/res/layout/layout_dm_base.xml b/app/src/main/res/layout/layout_dm_base.xml index 0207ebb8..710a0458 100644 --- a/app/src/main/res/layout/layout_dm_base.xml +++ b/app/src/main/res/layout/layout_dm_base.xml @@ -173,28 +173,34 @@ - + tools:visibility="visible"> + + + diff --git a/app/src/main/res/layout/layout_dm_user_item.xml b/app/src/main/res/layout/layout_dm_user_item.xml index 43b35af8..a8e156fb 100644 --- a/app/src/main/res/layout/layout_dm_user_item.xml +++ b/app/src/main/res/layout/layout_dm_user_item.xml @@ -78,12 +78,26 @@ android:duplicateParentState="true" android:visibility="gone" app:layout_constraintBottom_toBottomOf="@id/profile_pic" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/secondary_image" app:layout_constraintStart_toEndOf="@id/info" app:layout_constraintTop_toTopOf="@id/profile_pic" app:srcCompat="@drawable/ic_circle_check" app:tint="@color/ic_circle_check_tint" - tools:visibility="visible" /> + tools:visibility="gone" /> + + 80dp 48dp 4dp - 22dp + 24dp 6dp -12dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 677ad0c0..448e3293 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -397,4 +397,5 @@ Edit was unsuccessful Message Reply + Tap to remove