From 074ee18c9dbdc97a5a0620ebcbb4b812e7b24a3c Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Sun, 2 May 2021 18:16:25 +0900 Subject: [PATCH] Improve comments viewer ui and ux. Update ui for likes viewer and follows viewer. --- .../adapters/CommentsAdapter.java | 197 ++------ .../instagrabber/adapters/LikesAdapter.java | 2 +- .../viewholder/CommentViewHolder.java | 209 ++++++++ .../viewholder/FollowsViewHolder.java | 21 +- .../comments/ChildCommentViewHolder.java | 95 ---- .../comments/ParentCommentViewHolder.java | 95 ---- .../instagrabber/asyncs/CommentsFetcher.java | 268 ---------- .../customviews/ProfilePicView.java | 6 +- .../customviews/RamboTextViewV2.java | 24 + .../customviews/TextViewDrawableSize.java | 95 ++++ .../customviews/UsernameTextView.java | 77 +++ .../fragments/CommentsViewerFragment.java | 478 ------------------ .../fragments/LikesViewerFragment.java | 11 +- .../comments/CommentsViewerFragment.java | 237 +++++++++ .../fragments/comments/Helper.java | 288 +++++++++++ .../fragments/comments/RepliesFragment.java | 214 ++++++++ .../DirectMessageThreadFragment.java | 4 +- .../fragments/main/FeedFragment.java | 16 +- .../fragments/main/ProfileFragment.java | 3 +- .../awais/instagrabber/models/Comment.java | 117 +++++ .../instagrabber/models/CommentModel.java | 103 ---- .../GraphQLCommentsFetchResponse.java | 51 ++ .../java/awais/instagrabber/utils/Utils.java | 12 + .../viewmodels/AppStateViewModel.java | 2 + .../viewmodels/CommentsViewModel.java | 19 - .../viewmodels/CommentsViewerViewModel.java | 447 ++++++++++++++++ .../webservices/GraphQLService.java | 18 +- .../webservices/MediaService.java | 36 +- .../interceptors/LoggingInterceptor.java | 8 +- .../res/drawable/ic_outline_comments_24.xml | 12 +- .../res/drawable/ic_round_arrow_back_24.xml | 10 + .../res/drawable/ic_round_mode_comment_24.xml | 12 +- app/src/main/res/layout/fragment_comments.xml | 51 +- app/src/main/res/layout/fragment_likes.xml | 4 +- app/src/main/res/layout/item_comment.xml | 224 ++++---- .../main/res/layout/item_comment_small.xml | 109 ---- app/src/main/res/layout/item_follow.xml | 79 ++- .../main/res/menu/comment_options_menu.xml | 9 + .../res/navigation/comments_nav_graph.xml | 2 +- app/src/main/res/values/attrs.xml | 7 + app/src/main/res/values/color.xml | 9 +- app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/strings.xml | 6 +- app/src/main/res/values/styles.xml | 6 +- app/src/main/res/values/themes.xml | 7 +- 45 files changed, 2147 insertions(+), 1554 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java delete mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java delete mode 100644 app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java delete mode 100755 app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/TextViewDrawableSize.java create mode 100644 app/src/main/java/awais/instagrabber/customviews/UsernameTextView.java delete mode 100644 app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/comments/CommentsViewerFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/comments/Helper.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/comments/RepliesFragment.java create mode 100644 app/src/main/java/awais/instagrabber/models/Comment.java delete mode 100755 app/src/main/java/awais/instagrabber/models/CommentModel.java create mode 100644 app/src/main/java/awais/instagrabber/repositories/responses/GraphQLCommentsFetchResponse.java delete mode 100644 app/src/main/java/awais/instagrabber/viewmodels/CommentsViewModel.java create mode 100644 app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java create mode 100644 app/src/main/res/drawable/ic_round_arrow_back_24.xml delete mode 100755 app/src/main/res/layout/item_comment_small.xml create mode 100644 app/src/main/res/menu/comment_options_menu.xml diff --git a/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java index d88e5f34..d4de8ca6 100755 --- a/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java @@ -1,195 +1,60 @@ package awais.instagrabber.adapters; -import android.content.Context; import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; -import androidx.recyclerview.widget.RecyclerView; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.Objects; -import awais.instagrabber.adapters.viewholder.comments.ChildCommentViewHolder; -import awais.instagrabber.adapters.viewholder.comments.ParentCommentViewHolder; +import awais.instagrabber.adapters.viewholder.CommentViewHolder; import awais.instagrabber.databinding.ItemCommentBinding; -import awais.instagrabber.databinding.ItemCommentSmallBinding; -import awais.instagrabber.models.CommentModel; +import awais.instagrabber.models.Comment; -public final class CommentsAdapter extends ListAdapter { - private static final int TYPE_PARENT = 1; - private static final int TYPE_CHILD = 2; - - private final Map positionTypeMap = new HashMap<>(); - - // private final Filter filter = new Filter() { - // @NonNull - // @Override - // protected FilterResults performFiltering(final CharSequence filter) { - // final FilterResults results = new FilterResults(); - // results.values = commentModels; - // - // final int commentsLen = commentModels == null ? 0 : commentModels.size(); - // if (commentModels != null && commentsLen > 0 && !TextUtils.isEmpty(filter)) { - // final String query = filter.toString().toLowerCase(); - // final ArrayList filterList = new ArrayList<>(commentsLen); - // - // for (final CommentModel commentModel : commentModels) { - // final String commentText = commentModel.getText().toString().toLowerCase(); - // - // if (commentText.contains(query)) filterList.add(commentModel); - // else { - // final List childCommentModels = commentModel.getChildCommentModels(); - // if (childCommentModels != null) { - // for (final CommentModel childCommentModel : childCommentModels) { - // final String childCommentText = childCommentModel.getText().toString().toLowerCase(); - // if (childCommentText.contains(query)) filterList.add(commentModel); - // } - // } - // } - // } - // filterList.trimToSize(); - // results.values = filterList.toArray(new CommentModel[0]); - // } - // - // return results; - // } - // - // @Override - // protected void publishResults(final CharSequence constraint, @NonNull final FilterResults results) { - // if (results.values instanceof List) { - // //noinspection unchecked - // filteredCommentModels = (List) results.values; - // notifyDataSetChanged(); - // } - // } - // }; - - private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { +public final class CommentsAdapter extends ListAdapter { + private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { @Override - public boolean areItemsTheSame(@NonNull final CommentModel oldItem, @NonNull final CommentModel newItem) { - return oldItem.getId().equals(newItem.getId()); + public boolean areItemsTheSame(@NonNull final Comment oldItem, @NonNull final Comment newItem) { + return Objects.equals(oldItem.getId(), newItem.getId()); } @Override - public boolean areContentsTheSame(@NonNull final CommentModel oldItem, @NonNull final CommentModel newItem) { - return oldItem.getId().equals(newItem.getId()); + public boolean areContentsTheSame(@NonNull final Comment oldItem, @NonNull final Comment newItem) { + return Objects.equals(oldItem, newItem); } }; - private final CommentCallback commentCallback; - private CommentModel selected, toChangeLike; - private int selectedIndex, likedIndex; - public CommentsAdapter(final CommentCallback commentCallback) { + private final boolean showingReplies; + private final CommentCallback commentCallback; + private final long currentUserId; + + public CommentsAdapter(final long currentUserId, + final boolean showingReplies, + final CommentCallback commentCallback) { super(DIFF_CALLBACK); + this.showingReplies = showingReplies; + this.currentUserId = currentUserId; this.commentCallback = commentCallback; } @NonNull @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) { - final Context context = parent.getContext(); - final LayoutInflater layoutInflater = LayoutInflater.from(context); - if (type == TYPE_PARENT) { - final ItemCommentBinding binding = ItemCommentBinding.inflate(layoutInflater, parent, false); - return new ParentCommentViewHolder(binding); - } - final ItemCommentSmallBinding binding = ItemCommentSmallBinding.inflate(layoutInflater, parent, false); - return new ChildCommentViewHolder(binding); + public CommentViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) { + final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + final ItemCommentBinding binding = ItemCommentBinding.inflate(layoutInflater, parent, false); + return new CommentViewHolder(binding, currentUserId, commentCallback); } @Override - public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) { - CommentModel commentModel = getItem(position); - if (commentModel == null) return; - final int type = getItemViewType(position); - final boolean selected = this.selected != null && this.selected.getId().equals(commentModel.getId()); - final boolean toLike = this.toChangeLike != null && this.toChangeLike.getId().equals(commentModel.getId()); - if (toLike) commentModel = this.toChangeLike; - if (type == TYPE_PARENT) { - final ParentCommentViewHolder viewHolder = (ParentCommentViewHolder) holder; - viewHolder.bind(commentModel, selected, commentCallback); - return; - } - final ChildCommentViewHolder viewHolder = (ChildCommentViewHolder) holder; - viewHolder.bind(commentModel, selected, commentCallback); - } - - @Override - public void submitList(@Nullable final List list) { - final List flatList = flattenList(list); - super.submitList(flatList); - } - - @Override - public void submitList(@Nullable final List list, @Nullable final Runnable commitCallback) { - final List flatList = flattenList(list); - super.submitList(flatList, commitCallback); - } - - private List flattenList(final List list) { - if (list == null) { - return Collections.emptyList(); - } - final List flatList = new ArrayList<>(); - int lastCommentIndex = -1; - for (final CommentModel parent : list) { - lastCommentIndex++; - flatList.add(parent); - positionTypeMap.put(lastCommentIndex, TYPE_PARENT); - final List children = parent.getChildCommentModels(); - if (children != null) { - for (final CommentModel child : children) { - lastCommentIndex++; - flatList.add(child); - positionTypeMap.put(lastCommentIndex, TYPE_CHILD); - } - } - } - return flatList; - } - - - @Override - public int getItemViewType(final int position) { - final Integer type = positionTypeMap.get(position); - if (type == null) { - return TYPE_PARENT; - } - return type; - } - - public void setSelected(final CommentModel commentModel) { - this.selected = commentModel; - selectedIndex = getCurrentList().indexOf(commentModel); - notifyItemChanged(selectedIndex); - } - - public void clearSelection() { - this.selected = null; - notifyItemChanged(selectedIndex); - } - - public void setLiked(final CommentModel commentModel, final boolean liked) { - likedIndex = getCurrentList().indexOf(commentModel); - CommentModel newCommentModel = commentModel; - newCommentModel.setLiked(liked); - this.toChangeLike = newCommentModel; - notifyItemChanged(likedIndex); - } - - public CommentModel getSelected() { - return selected; + public void onBindViewHolder(@NonNull final CommentViewHolder holder, final int position) { + final Comment comment = getItem(position); + holder.bind(comment, showingReplies && position == 0, showingReplies && position != 0); } public interface CommentCallback { - void onClick(final CommentModel comment); + void onClick(final Comment comment); void onHashtagClick(final String hashtag); @@ -198,5 +63,15 @@ public final class CommentsAdapter extends ListAdapter @Override public void onBindViewHolder(@NonNull final FollowsViewHolder holder, final int position) { final User model = profileModels.get(position); - holder.bind(model, null, onClickListener); + holder.bind(model, onClickListener); } @Override diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java new file mode 100644 index 00000000..151d308c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java @@ -0,0 +1,209 @@ +package awais.instagrabber.adapters.viewholder; + +import android.content.Context; +import android.content.res.Resources; +import android.util.TypedValue; +import android.view.Menu; +import android.view.View; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.appcompat.view.ContextThemeWrapper; +import androidx.appcompat.widget.PopupMenu; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.CommentsAdapter.CommentCallback; +import awais.instagrabber.customviews.ProfilePicView; +import awais.instagrabber.databinding.ItemCommentBinding; +import awais.instagrabber.models.Comment; +import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.utils.Utils; + +public final class CommentViewHolder extends RecyclerView.ViewHolder { + + private final ItemCommentBinding binding; + private final long currentUserId; + private final CommentCallback commentCallback; + @ColorInt + private int parentCommentHighlightColor; + private PopupMenu optionsPopup; + + public CommentViewHolder(@NonNull final ItemCommentBinding binding, + final long currentUserId, + final CommentCallback commentCallback) { + super(binding.getRoot()); + this.binding = binding; + this.currentUserId = currentUserId; + this.commentCallback = commentCallback; + final Context context = itemView.getContext(); + if (context == null) return; + final Resources.Theme theme = context.getTheme(); + if (theme == null) return; + final TypedValue typedValue = new TypedValue(); + final boolean resolved = theme.resolveAttribute(R.attr.parentCommentHighlightColor, typedValue, true); + if (resolved) { + parentCommentHighlightColor = typedValue.data; + } + } + + public void bind(final Comment comment, final boolean isReplyParent, final boolean isReply) { + if (comment == null) return; + itemView.setOnClickListener(v -> { + if (commentCallback != null) { + commentCallback.onClick(comment); + } + }); + if (isReplyParent && parentCommentHighlightColor != 0) { + itemView.setBackgroundColor(parentCommentHighlightColor); + } else { + itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent)); + } + setupCommentText(comment, isReply); + binding.date.setText(comment.getDateTime()); + setLikes(comment, isReply); + setReplies(comment, isReply); + setUser(comment, isReply); + setupOptions(comment, isReply); + } + + private void setupCommentText(@NonNull final Comment comment, final boolean isReply) { + binding.comment.clearOnURLClickListeners(); + binding.comment.clearOnHashtagClickListeners(); + binding.comment.clearOnMentionClickListeners(); + binding.comment.clearOnEmailClickListeners(); + binding.comment.setText(comment.getText()); + binding.comment.setTextSize(TypedValue.COMPLEX_UNIT_SP, isReply ? 12 : 14); + binding.comment.addOnHashtagListener(autoLinkItem -> { + final String originalText = autoLinkItem.getOriginalText(); + if (commentCallback == null) return; + commentCallback.onHashtagClick(originalText); + }); + binding.comment.addOnMentionClickListener(autoLinkItem -> { + final String originalText = autoLinkItem.getOriginalText(); + if (commentCallback == null) return; + commentCallback.onMentionClick(originalText); + + }); + binding.comment.addOnEmailClickListener(autoLinkItem -> { + final String originalText = autoLinkItem.getOriginalText(); + if (commentCallback == null) return; + commentCallback.onEmailClick(originalText); + }); + binding.comment.addOnURLClickListener(autoLinkItem -> { + final String originalText = autoLinkItem.getOriginalText(); + if (commentCallback == null) return; + commentCallback.onURLClick(originalText); + }); + binding.comment.setOnLongClickListener(v -> { + Utils.copyText(itemView.getContext(), comment.getText()); + return true; + }); + binding.comment.setOnClickListener(v -> commentCallback.onClick(comment)); + } + + private void setUser(@NonNull final Comment comment, final boolean isReply) { + final User user = comment.getUser(); + if (user == null) return; + binding.username.setUsername(user.getUsername(), user.isVerified()); + binding.username.setTextAppearance(itemView.getContext(), isReply ? R.style.TextAppearance_MaterialComponents_Subtitle2 + : R.style.TextAppearance_MaterialComponents_Subtitle1); + binding.username.setOnClickListener(v -> { + if (commentCallback == null) return; + commentCallback.onMentionClick("@" + user.getUsername()); + }); + binding.profilePic.setImageURI(user.getProfilePicUrl()); + binding.profilePic.setSize(isReply ? ProfilePicView.Size.SMALLER : ProfilePicView.Size.SMALL); + binding.profilePic.setOnClickListener(v -> { + if (commentCallback == null) return; + commentCallback.onMentionClick("@" + user.getUsername()); + }); + } + + private void setLikes(@NonNull final Comment comment, final boolean isReply) { + // final String likesString = itemView.getResources().getQuantityString(R.plurals.likes_count, likes, likes); + binding.likes.setText(String.valueOf(comment.getLikes())); + binding.likes.setOnLongClickListener(v -> { + if (commentCallback == null) return false; + commentCallback.onViewLikes(comment); + return true; + }); + if (currentUserId == 0) { // not logged in + binding.likes.setOnClickListener(v -> { + if (commentCallback == null) return; + commentCallback.onViewLikes(comment); + }); + return; + } + final boolean liked = comment.getLiked(); + final int resId = liked ? R.drawable.ic_like : R.drawable.ic_not_liked; + binding.likes.setCompoundDrawablesRelativeWithSize(ContextCompat.getDrawable(itemView.getContext(), resId), null, null, null); + binding.likes.setOnClickListener(v -> { + if (commentCallback == null) return; + // toggle like + commentCallback.onLikeClick(comment, !liked, isReply); + }); + } + + private void setReplies(@NonNull final Comment comment, final boolean isReply) { + final int replies = comment.getReplyCount(); + binding.replies.setVisibility(View.VISIBLE); + final String text = isReply ? "" : String.valueOf(replies); + // final String string = itemView.getResources().getQuantityString(R.plurals.replies_count, replies, replies); + binding.replies.setText(text); + binding.replies.setOnClickListener(v -> { + if (commentCallback == null) return; + commentCallback.onRepliesClick(comment); + }); + } + + private void setupOptions(final Comment comment, final boolean isReply) { + binding.options.setOnClickListener(v -> { + if (optionsPopup == null) { + createOptionsPopupMenu(comment, isReply); + } + if (optionsPopup == null) return; + optionsPopup.show(); + }); + } + + private void createOptionsPopupMenu(final Comment comment, final boolean isReply) { + if (optionsPopup == null) { + final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(itemView.getContext(), R.style.popupMenuStyle); + optionsPopup = new PopupMenu(themeWrapper, binding.options); + } else { + optionsPopup.getMenu().clear(); + } + optionsPopup.getMenuInflater().inflate(R.menu.comment_options_menu, optionsPopup.getMenu()); + final User user = comment.getUser(); + if (currentUserId == 0 || user == null || user.getPk() != currentUserId) { + final Menu menu = optionsPopup.getMenu(); + menu.removeItem(R.id.delete); + } + optionsPopup.setOnMenuItemClickListener(item -> { + if (commentCallback == null) return false; + int itemId = item.getItemId(); + if (itemId == R.id.translate) { + commentCallback.onTranslate(comment); + return true; + } + if (itemId == R.id.delete) { + commentCallback.onDelete(comment, isReply); + } + return true; + }); + } + + // private void setupReply(final Comment comment) { + // if (!isLoggedIn) { + // binding.reply.setVisibility(View.GONE); + // return; + // } + // binding.reply.setOnClickListener(v -> { + // if (commentCallback == null) return; + // // toggle like + // commentCallback.onReplyClick(comment); + // }); + // } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java index 6f48be53..1274bd2b 100755 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java @@ -2,10 +2,9 @@ package awais.instagrabber.adapters.viewholder; import android.view.View; +import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; -import java.util.List; - import awais.instagrabber.databinding.ItemFollowBinding; import awais.instagrabber.models.FollowModel; import awais.instagrabber.repositories.responses.User; @@ -14,23 +13,19 @@ public final class FollowsViewHolder extends RecyclerView.ViewHolder { private final ItemFollowBinding binding; - public FollowsViewHolder(final ItemFollowBinding binding) { + public FollowsViewHolder(@NonNull final ItemFollowBinding binding) { super(binding.getRoot()); this.binding = binding; } public void bind(final User model, - final List admins, final View.OnClickListener onClickListener) { if (model == null) return; itemView.setTag(model); itemView.setOnClickListener(onClickListener); - binding.tvUsername.setText(model.getUsername()); - binding.tvFullName.setText(model.getFullName()); - if (admins != null && admins.contains(model.getPk())) { - binding.isAdmin.setVisibility(View.VISIBLE); - } - binding.ivProfilePic.setImageURI(model.getProfilePicUrl()); + binding.username.setUsername("@" + model.getUsername(), model.isVerified()); + binding.fullName.setText(model.getFullName()); + binding.profilePic.setImageURI(model.getProfilePicUrl()); } public void bind(final FollowModel model, @@ -38,8 +33,8 @@ public final class FollowsViewHolder extends RecyclerView.ViewHolder { if (model == null) return; itemView.setTag(model); itemView.setOnClickListener(onClickListener); - binding.tvUsername.setText(model.getUsername()); - binding.tvFullName.setText(model.getFullName()); - binding.ivProfilePic.setImageURI(model.getProfilePicUrl()); + binding.username.setUsername("@" + model.getUsername()); + binding.fullName.setText(model.getFullName()); + binding.profilePic.setImageURI(model.getProfilePicUrl()); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java deleted file mode 100644 index fd4cfb12..00000000 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java +++ /dev/null @@ -1,95 +0,0 @@ -package awais.instagrabber.adapters.viewholder.comments; - -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import awais.instagrabber.R; -import awais.instagrabber.adapters.CommentsAdapter.CommentCallback; -import awais.instagrabber.databinding.ItemCommentSmallBinding; -import awais.instagrabber.models.CommentModel; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.utils.Utils; - -public final class ChildCommentViewHolder extends RecyclerView.ViewHolder { - - private final ItemCommentSmallBinding binding; - - public ChildCommentViewHolder(@NonNull final ItemCommentSmallBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - - public void bind(final CommentModel comment, - final boolean selected, - final CommentCallback commentCallback) { - if (comment == null) return; - if (commentCallback != null) { - itemView.setOnClickListener(v -> commentCallback.onClick(comment)); - } - if (selected) { - itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_selected)); - } else { - itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent)); - } - setupCommentText(comment, commentCallback); - binding.tvDate.setText(comment.getDateTime()); - setLiked(comment.getLiked()); - setLikes((int) comment.getLikes()); - setUser(comment); - } - - private void setupCommentText(final CommentModel comment, final CommentCallback commentCallback) { - binding.tvComment.clearOnURLClickListeners(); - binding.tvComment.clearOnHashtagClickListeners(); - binding.tvComment.clearOnMentionClickListeners(); - binding.tvComment.clearOnEmailClickListeners(); - binding.tvComment.setText(comment.getText()); - binding.tvComment.addOnHashtagListener(autoLinkItem -> { - final String originalText = autoLinkItem.getOriginalText(); - if (commentCallback == null) return; - commentCallback.onHashtagClick(originalText); - }); - binding.tvComment.addOnMentionClickListener(autoLinkItem -> { - final String originalText = autoLinkItem.getOriginalText(); - if (commentCallback == null) return; - commentCallback.onMentionClick(originalText); - - }); - binding.tvComment.addOnEmailClickListener(autoLinkItem -> { - final String originalText = autoLinkItem.getOriginalText(); - if (commentCallback == null) return; - commentCallback.onEmailClick(originalText); - }); - binding.tvComment.addOnURLClickListener(autoLinkItem -> { - final String originalText = autoLinkItem.getOriginalText(); - if (commentCallback == null) return; - commentCallback.onURLClick(originalText); - }); - binding.tvComment.setOnLongClickListener(v -> { - Utils.copyText(itemView.getContext(), comment.getText()); - return true; - }); - binding.tvComment.setOnClickListener(v -> commentCallback.onClick(comment)); - } - - private void setUser(final CommentModel comment) { - final User profileModel = comment.getProfileModel(); - if (profileModel == null) return; - binding.tvUsername.setText(profileModel.getUsername()); - binding.ivProfilePic.setImageURI(profileModel.getProfilePicUrl()); - binding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE); - } - - private void setLikes(final int likes) { - final String likesString = itemView.getResources().getQuantityString(R.plurals.likes_count, likes, likes); - binding.tvLikes.setText(likesString); - } - - public final void setLiked(final boolean liked) { - if (liked) { - itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_liked)); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java deleted file mode 100644 index 31edac56..00000000 --- a/app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java +++ /dev/null @@ -1,95 +0,0 @@ -package awais.instagrabber.adapters.viewholder.comments; - -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import awais.instagrabber.R; -import awais.instagrabber.adapters.CommentsAdapter.CommentCallback; -import awais.instagrabber.databinding.ItemCommentBinding; -import awais.instagrabber.models.CommentModel; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.utils.Utils; - -public final class ParentCommentViewHolder extends RecyclerView.ViewHolder { - - private final ItemCommentBinding binding; - - public ParentCommentViewHolder(@NonNull final ItemCommentBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - - public void bind(final CommentModel comment, - final boolean selected, - final CommentCallback commentCallback) { - if (comment == null) return; - if (commentCallback != null) { - itemView.setOnClickListener(v -> commentCallback.onClick(comment)); - } - if (selected) { - itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_selected)); - } else { - itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent)); - } - setupCommentText(comment, commentCallback); - binding.tvDate.setText(comment.getDateTime()); - setLiked(comment.getLiked()); - setLikes((int) comment.getLikes()); - setUser(comment); - } - - private void setupCommentText(final CommentModel comment, final CommentCallback commentCallback) { - binding.tvComment.clearOnURLClickListeners(); - binding.tvComment.clearOnHashtagClickListeners(); - binding.tvComment.clearOnMentionClickListeners(); - binding.tvComment.clearOnEmailClickListeners(); - binding.tvComment.setText(comment.getText()); - binding.tvComment.addOnHashtagListener(autoLinkItem -> { - final String originalText = autoLinkItem.getOriginalText(); - if (commentCallback == null) return; - commentCallback.onHashtagClick(originalText); - }); - binding.tvComment.addOnMentionClickListener(autoLinkItem -> { - final String originalText = autoLinkItem.getOriginalText(); - if (commentCallback == null) return; - commentCallback.onMentionClick(originalText); - - }); - binding.tvComment.addOnEmailClickListener(autoLinkItem -> { - final String originalText = autoLinkItem.getOriginalText(); - if (commentCallback == null) return; - commentCallback.onEmailClick(originalText); - }); - binding.tvComment.addOnURLClickListener(autoLinkItem -> { - final String originalText = autoLinkItem.getOriginalText(); - if (commentCallback == null) return; - commentCallback.onURLClick(originalText); - }); - binding.tvComment.setOnLongClickListener(v -> { - Utils.copyText(itemView.getContext(), comment.getText()); - return true; - }); - binding.tvComment.setOnClickListener(v -> commentCallback.onClick(comment)); - } - - private void setUser(final CommentModel comment) { - final User profileModel = comment.getProfileModel(); - if (profileModel == null) return; - binding.tvUsername.setText(profileModel.getUsername()); - binding.ivProfilePic.setImageURI(profileModel.getProfilePicUrl()); - binding.isVerified.setVisibility(profileModel.isVerified() ? View.VISIBLE : View.GONE); - } - - private void setLikes(final int likes) { - final String likesString = itemView.getResources().getQuantityString(R.plurals.likes_count, likes, likes); - binding.tvLikes.setText(likesString); - } - - public final void setLiked(final boolean liked) { - if (liked) { - itemView.setBackgroundColor(itemView.getResources().getColor(R.color.comment_liked)); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java deleted file mode 100755 index b75b758c..00000000 --- a/app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java +++ /dev/null @@ -1,268 +0,0 @@ -package awais.instagrabber.asyncs; - -import android.os.AsyncTask; -import android.util.Log; -import android.util.Pair; - -import androidx.annotation.NonNull; - -import org.json.JSONArray; -import org.json.JSONObject; - -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -import awais.instagrabber.BuildConfig; -import awais.instagrabber.interfaces.FetchListener; -import awais.instagrabber.models.CommentModel; -import awais.instagrabber.repositories.responses.FriendshipStatus; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.NetworkUtils; -import awais.instagrabber.utils.TextUtils; -//import awaisomereport.LogCollector; - -//import static awais.instagrabber.utils.Utils.logCollector; - -public final class CommentsFetcher extends AsyncTask> { - private static final String TAG = "CommentsFetcher"; - - private final String shortCode, endCursor; - private final FetchListener> fetchListener; - - public CommentsFetcher(final String shortCode, final String endCursor, final FetchListener> fetchListener) { - this.shortCode = shortCode; - this.endCursor = endCursor; - this.fetchListener = fetchListener; - } - - @NonNull - @Override - protected List doInBackground(final Void... voids) { - /* - "https://www.instagram.com/graphql/query/?query_hash=97b41c52301f77ce508f55e66d17620e&variables=" + "{\"shortcode\":\"" + shortcode + "\",\"first\":50,\"after\":\"" + endCursor + "\"}"; - - 97b41c52301f77ce508f55e66d17620e -> for comments - 51fdd02b67508306ad4484ff574a0b62 -> for child comments - - https://www.instagram.com/graphql/query/?query_hash=51fdd02b67508306ad4484ff574a0b62&variables={"comment_id":"18100041898085322","first":50,"after":""} - */ - final List commentModels = getParentComments(); - if (commentModels != null) { - for (final CommentModel commentModel : commentModels) { - final List childCommentModels = commentModel.getChildCommentModels(); - if (childCommentModels != null) { - final int childCommentsLen = childCommentModels.size(); - final CommentModel lastChild = childCommentModels.get(childCommentsLen - 1); - if (lastChild != null && lastChild.hasNextPage() && !TextUtils.isEmpty(lastChild.getEndCursor())) { - final List remoteChildComments = getChildComments(commentModel.getId()); - commentModel.setChildCommentModels(remoteChildComments); - lastChild.setPageCursor(false, null); - } - } - } - } - return commentModels; - } - - @Override - protected void onPreExecute() { - if (fetchListener != null) fetchListener.doBefore(); - } - - @Override - protected void onPostExecute(final List result) { - if (fetchListener != null) fetchListener.onResult(result); - } - - @NonNull - private synchronized List getChildComments(final String commentId) { - final List commentModels = new ArrayList<>(); - String childEndCursor = ""; - while (childEndCursor != null) { - final String url = "https://www.instagram.com/graphql/query/?query_hash=51fdd02b67508306ad4484ff574a0b62&variables=" + - "{\"comment_id\":\"" + commentId + "\",\"first\":50,\"after\":\"" + childEndCursor + "\"}"; - try { - final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); - conn.setUseCaches(false); - conn.connect(); - - if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) break; - else { - final JSONObject data = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONObject("data") - .getJSONObject("comment") - .getJSONObject("edge_threaded_comments"); - - final JSONObject pageInfo = data.getJSONObject("page_info"); - childEndCursor = pageInfo.getString("end_cursor"); - if (TextUtils.isEmpty(childEndCursor)) childEndCursor = null; - - final JSONArray childComments = data.optJSONArray("edges"); - if (childComments != null) { - final int length = childComments.length(); - for (int i = 0; i < length; ++i) { - final JSONObject childComment = childComments.getJSONObject(i).optJSONObject("node"); - - if (childComment != null) { - final JSONObject owner = childComment.getJSONObject("owner"); - final User user = new User( - owner.optLong(Constants.EXTRAS_ID, 0), - owner.getString(Constants.EXTRAS_USERNAME), - null, - false, - owner.getString("profile_pic_url"), - null, - new FriendshipStatus(false, false, false, false, false, false, false, false, false, false), - false, false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, null, - null, null, null); - final JSONObject likedBy = childComment.optJSONObject("edge_liked_by"); - commentModels.add(new CommentModel(childComment.getString(Constants.EXTRAS_ID), - childComment.getString("text"), - childComment.getLong("created_at"), - likedBy != null ? likedBy.optLong("count", 0) : 0, - childComment.getBoolean("viewer_has_liked"), - user)); - } - } - } - } - conn.disconnect(); - } catch (final Exception e) { -// if (logCollector != null) -// logCollector.appendException(e, -// LogCollector.LogFile.ASYNC_COMMENTS_FETCHER, -// "getChildComments", -// new Pair<>("commentModels.size", commentModels.size())); - if (BuildConfig.DEBUG) Log.e(TAG, "", e); - if (fetchListener != null) fetchListener.onFailure(e); - break; - } - } - - return commentModels; - } - - @NonNull - private synchronized List getParentComments() { - final List commentModels = new ArrayList<>(); - final String url = "https://www.instagram.com/graphql/query/?query_hash=bc3296d1ce80a24b1b6e40b1e72903f5&variables=" + - "{\"shortcode\":\"" + shortCode + "\",\"first\":50,\"after\":\"" + endCursor.replace("\"", "\\\"") + "\"}"; - try { - final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); - conn.setUseCaches(false); - conn.connect(); - - if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) return null; - else { - final JSONObject parentComments = new JSONObject(NetworkUtils.readFromConnection(conn)).getJSONObject("data") - .getJSONObject("shortcode_media") - .getJSONObject( - "edge_media_to_parent_comment"); - - final JSONObject pageInfo = parentComments.getJSONObject("page_info"); - final String foundEndCursor = pageInfo.optString("end_cursor"); - final boolean hasNextPage = pageInfo.optBoolean("has_next_page", !TextUtils.isEmpty(foundEndCursor)); - - // final boolean containsToken = endCursor.contains("bifilter_token"); - // if (!Utils.isEmpty(endCursor) && (containsToken || endCursor.contains("cached_comments_cursor"))) { - // final JSONObject endCursorObject = new JSONObject(endCursor); - // endCursor = endCursorObject.optString("cached_comments_cursor"); - // - // if (!Utils.isEmpty(endCursor)) - // endCursor = "{\\\"cached_comments_cursor\\\": \\\"" + endCursor + "\\\", "; - // else - // endCursor = "{"; - // - // endCursor = endCursor + "\\\"bifilter_token\\\": \\\"" + endCursorObject.getString("bifilter_token") + "\\\"}"; - // } - // else if (containsToken) endCursor = null; - - final JSONArray comments = parentComments.getJSONArray("edges"); - final int commentsLen = comments.length(); - for (int i = 0; i < commentsLen; ++i) { - final JSONObject comment = comments.getJSONObject(i).getJSONObject("node"); - - final JSONObject owner = comment.getJSONObject("owner"); - final User user = new User( - owner.optLong(Constants.EXTRAS_ID, 0), - owner.getString(Constants.EXTRAS_USERNAME), - null, - false, - owner.getString("profile_pic_url"), - null, - new FriendshipStatus(false, false, false, false, false, false, false, false, false, false), - owner.optBoolean("is_verified"), - false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, null, null, - null, null); - final JSONObject likedBy = comment.optJSONObject("edge_liked_by"); - final String commentId = comment.getString(Constants.EXTRAS_ID); - final CommentModel commentModel = new CommentModel(commentId, - comment.getString("text"), - comment.getLong("created_at"), - likedBy != null ? likedBy.optLong("count", 0) : 0, - comment.getBoolean("viewer_has_liked"), - user); - if (i == 0 && !foundEndCursor.contains("tao_cursor")) - commentModel.setPageCursor(hasNextPage, TextUtils.isEmpty(foundEndCursor) ? null : foundEndCursor); - JSONObject tempJsonObject; - final JSONArray childCommentsArray; - final int childCommentsLen; - if ((tempJsonObject = comment.optJSONObject("edge_threaded_comments")) != null && - (childCommentsArray = tempJsonObject.optJSONArray("edges")) != null - && (childCommentsLen = childCommentsArray.length()) > 0) { - - final String childEndCursor; - final boolean childHasNextPage; - if ((tempJsonObject = tempJsonObject.optJSONObject("page_info")) != null) { - childEndCursor = tempJsonObject.optString("end_cursor"); - childHasNextPage = tempJsonObject.optBoolean("has_next_page", !TextUtils.isEmpty(childEndCursor)); - } else { - childEndCursor = null; - childHasNextPage = false; - } - - final List childCommentModels = new ArrayList<>(); - for (int j = 0; j < childCommentsLen; ++j) { - final JSONObject childComment = childCommentsArray.getJSONObject(j).getJSONObject("node"); - - tempJsonObject = childComment.getJSONObject("owner"); - final User childUser = new User( - tempJsonObject.optLong(Constants.EXTRAS_ID, 0), - tempJsonObject.getString(Constants.EXTRAS_USERNAME), - null, - false, - tempJsonObject.getString("profile_pic_url"), - null, - new FriendshipStatus(false, false, false, false, false, false, false, false, false, false), - tempJsonObject.optBoolean("is_verified"), false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, - null, null, null, null, null, null); - - tempJsonObject = childComment.optJSONObject("edge_liked_by"); - childCommentModels.add(new CommentModel(childComment.getString(Constants.EXTRAS_ID), - childComment.getString("text"), - childComment.getLong("created_at"), - tempJsonObject != null ? tempJsonObject.optLong("count", 0) : 0, - childComment.getBoolean("viewer_has_liked"), - childUser)); - } - childCommentModels.get(childCommentsLen - 1).setPageCursor(childHasNextPage, childEndCursor); - commentModel.setChildCommentModels(childCommentModels); - } - commentModels.add(commentModel); - } - } - - conn.disconnect(); - } catch (final Exception e) { -// if (logCollector != null) -// logCollector.appendException(e, LogCollector.LogFile.ASYNC_COMMENTS_FETCHER, "getParentComments", -// new Pair<>("commentModelsList.size", commentModels.size())); - if (BuildConfig.DEBUG) Log.e("AWAISKING_APP", "", e); - if (fetchListener != null) fetchListener.onFailure(e); - return null; - } - return commentModels; - } -} diff --git a/app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java b/app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java index 2f8409de..491127a6 100644 --- a/app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java +++ b/app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java @@ -70,6 +70,9 @@ public final class ProfilePicView extends CircularImageView { case SMALL: dimenRes = R.dimen.profile_pic_size_small; break; + case SMALLER: + dimenRes = R.dimen.profile_pic_size_smaller; + break; case TINY: dimenRes = R.dimen.profile_pic_size_tiny; break; @@ -113,7 +116,8 @@ public final class ProfilePicView extends CircularImageView { TINY(0), SMALL(1), REGULAR(2), - LARGE(3); + LARGE(3), + SMALLER(4); private final int value; private static final Map map = new HashMap<>(); diff --git a/app/src/main/java/awais/instagrabber/customviews/RamboTextViewV2.java b/app/src/main/java/awais/instagrabber/customviews/RamboTextViewV2.java index 30c3646f..a96766af 100644 --- a/app/src/main/java/awais/instagrabber/customviews/RamboTextViewV2.java +++ b/app/src/main/java/awais/instagrabber/customviews/RamboTextViewV2.java @@ -1,10 +1,12 @@ package awais.instagrabber.customviews; import android.content.Context; +import android.text.InputFilter; import android.util.AttributeSet; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.emoji.widget.EmojiTextViewHelper; import java.util.ArrayList; import java.util.List; @@ -23,6 +25,8 @@ public class RamboTextViewV2 extends AutoLinkTextView { private final List onURLClickListeners = new ArrayList<>(); private final List onEmailClickListeners = new ArrayList<>(); + private EmojiTextViewHelper emojiTextViewHelper; + public RamboTextViewV2(@NonNull final Context context, @Nullable final AttributeSet attrs) { super(context, attrs); @@ -30,6 +34,7 @@ public class RamboTextViewV2 extends AutoLinkTextView { } private void init() { + getEmojiTextViewHelper().updateTransformationMethod(); addAutoLinkMode(MODE_HASHTAG.INSTANCE, MODE_MENTION.INSTANCE, MODE_EMAIL.INSTANCE, MODE_URL.INSTANCE); onAutoLinkClick(autoLinkItem -> { final Mode mode = autoLinkItem.getMode(); @@ -60,6 +65,25 @@ public class RamboTextViewV2 extends AutoLinkTextView { onAutoLinkLongClick(autoLinkItem -> {}); } + @Override + public void setFilters(InputFilter[] filters) { + super.setFilters(getEmojiTextViewHelper().getFilters(filters)); + } + + @Override + public void setAllCaps(boolean allCaps) { + super.setAllCaps(allCaps); + getEmojiTextViewHelper().setAllCaps(allCaps); + } + + + private EmojiTextViewHelper getEmojiTextViewHelper() { + if (emojiTextViewHelper == null) { + emojiTextViewHelper = new EmojiTextViewHelper(this); + } + return emojiTextViewHelper; + } + public void addOnMentionClickListener(final OnMentionClickListener onMentionClickListener) { if (onMentionClickListener == null) { return; diff --git a/app/src/main/java/awais/instagrabber/customviews/TextViewDrawableSize.java b/app/src/main/java/awais/instagrabber/customviews/TextViewDrawableSize.java new file mode 100644 index 00000000..d4c96b8c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/TextViewDrawableSize.java @@ -0,0 +1,95 @@ +package awais.instagrabber.customviews; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.emoji.widget.EmojiAppCompatTextView; + +import awais.instagrabber.R; + +/** + * https://stackoverflow.com/a/31916731 + */ +public class TextViewDrawableSize extends EmojiAppCompatTextView { + + private int mDrawableWidth; + private int mDrawableHeight; + private boolean calledFromInit = false; + + public TextViewDrawableSize(final Context context) { + this(context, null); + } + + public TextViewDrawableSize(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public TextViewDrawableSize(final Context context, final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr); + } + + private void init(@NonNull final Context context, final AttributeSet attrs, final int defStyleAttr) { + final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TextViewDrawableSize, defStyleAttr, 0); + + try { + mDrawableWidth = array.getDimensionPixelSize(R.styleable.TextViewDrawableSize_compoundDrawableWidth, -1); + mDrawableHeight = array.getDimensionPixelSize(R.styleable.TextViewDrawableSize_compoundDrawableHeight, -1); + } finally { + array.recycle(); + } + + if (mDrawableWidth > 0 || mDrawableHeight > 0) { + initCompoundDrawableSize(); + } + } + + private void initCompoundDrawableSize() { + final Drawable[] drawables = getCompoundDrawablesRelative(); + for (Drawable drawable : drawables) { + if (drawable == null) { + continue; + } + + final Rect realBounds = drawable.getBounds(); + float scaleFactor = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth(); + + float drawableWidth = drawable.getIntrinsicWidth(); + float drawableHeight = drawable.getIntrinsicHeight(); + + if (mDrawableWidth > 0) { + // save scale factor of image + if (drawableWidth > mDrawableWidth) { + drawableWidth = mDrawableWidth; + drawableHeight = drawableWidth * scaleFactor; + } + } + if (mDrawableHeight > 0) { + // save scale factor of image + if (drawableHeight > mDrawableHeight) { + drawableHeight = mDrawableHeight; + drawableWidth = drawableHeight / scaleFactor; + } + } + + realBounds.right = realBounds.left + Math.round(drawableWidth); + realBounds.bottom = realBounds.top + Math.round(drawableHeight); + + drawable.setBounds(realBounds); + } + setCompoundDrawablesRelative(drawables[0], drawables[1], drawables[2], drawables[3]); + } + + public void setCompoundDrawablesRelativeWithSize(@Nullable final Drawable start, + @Nullable final Drawable top, + @Nullable final Drawable end, + @Nullable final Drawable bottom) { + setCompoundDrawablesRelative(start, top, end, bottom); + initCompoundDrawableSize(); + } +} diff --git a/app/src/main/java/awais/instagrabber/customviews/UsernameTextView.java b/app/src/main/java/awais/instagrabber/customviews/UsernameTextView.java new file mode 100644 index 00000000..c2da60c3 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/customviews/UsernameTextView.java @@ -0,0 +1,77 @@ +package awais.instagrabber.customviews; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.util.AttributeSet; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.appcompat.widget.AppCompatTextView; + +import awais.instagrabber.R; +import awais.instagrabber.utils.Utils; + +public class UsernameTextView extends AppCompatTextView { + private static final String TAG = UsernameTextView.class.getSimpleName(); + + private final int drawableSize = Utils.convertDpToPx(24); + + private boolean verified; + private VerticalImageSpan verifiedSpan; + + public UsernameTextView(@NonNull final Context context) { + this(context, null); + } + + public UsernameTextView(@NonNull final Context context, @Nullable final AttributeSet attrs) { + this(context, attrs, 0); + } + + public UsernameTextView(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + try { + final Drawable verifiedDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.verified); + final Drawable drawable = verifiedDrawable.mutate(); + drawable.setBounds(0, 0, drawableSize, drawableSize); + verifiedSpan = new VerticalImageSpan(drawable); + } catch (Exception e) { + Log.e(TAG, "init: ", e); + } + } + + public void setUsername(final CharSequence username) { + setUsername(username, false); + } + + public void setUsername(final CharSequence username, final boolean verified) { + this.verified = verified; + final SpannableStringBuilder sb = new SpannableStringBuilder(username); + if (verified) { + try { + if (verifiedSpan != null) { + sb.append(" "); + sb.setSpan(verifiedSpan, sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } catch (Exception e) { + Log.e(TAG, "bind: ", e); + } + } + super.setText(sb); + } + + public boolean isVerified() { + return verified; + } + + public void setVerified(final boolean verified) { + setUsername(getText(), verified); + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java deleted file mode 100644 index d8635892..00000000 --- a/app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java +++ /dev/null @@ -1,478 +0,0 @@ -package awais.instagrabber.fragments; - -import android.content.Context; -import android.content.DialogInterface; -import android.content.res.Resources; -import android.os.AsyncTask; -import android.os.Bundle; -import android.text.Editable; -import android.text.SpannableString; -import android.text.Spanned; -import android.text.TextWatcher; -import android.text.style.RelativeSizeSpan; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.LinearLayoutCompat; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.NavController; -import androidx.navigation.NavDirections; -import androidx.navigation.fragment.NavHostFragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; - -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import awais.instagrabber.BuildConfig; -import awais.instagrabber.R; -import awais.instagrabber.adapters.CommentsAdapter; -import awais.instagrabber.asyncs.CommentsFetcher; -import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; -import awais.instagrabber.databinding.FragmentCommentsBinding; -import awais.instagrabber.interfaces.FetchListener; -import awais.instagrabber.models.CommentModel; -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.utils.Constants; -import awais.instagrabber.utils.CookieUtils; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; -import awais.instagrabber.viewmodels.CommentsViewModel; -import awais.instagrabber.webservices.MediaService; -import awais.instagrabber.webservices.ServiceCallback; - -import static android.content.Context.INPUT_METHOD_SERVICE; - -public final class CommentsViewerFragment extends BottomSheetDialogFragment implements SwipeRefreshLayout.OnRefreshListener { - private static final String TAG = "CommentsViewerFragment"; - - private final String cookie = Utils.settingsHelper.getString(Constants.COOKIE); - - private CommentsAdapter commentsAdapter; - private FragmentCommentsBinding binding; - private RecyclerLazyLoader lazyLoader; - private String shortCode; - private long authorUserId, userIdFromCookie; - private String endCursor = null; - private Resources resources; - private InputMethodManager imm; - private LinearLayoutCompat root; - private boolean shouldRefresh = true, hasNextPage = false; - private MediaService mediaService; - private String postId; - private AsyncTask> currentlyRunning; - private CommentsViewModel commentsViewModel; - - private final FetchListener> fetchListener = new FetchListener>() { - @Override - public void doBefore() { - binding.swipeRefreshLayout.setRefreshing(true); - } - - @Override - public void onResult(final List commentModels) { - if (commentModels != null && commentModels.size() > 0) { - endCursor = commentModels.get(0).getEndCursor(); - hasNextPage = commentModels.get(0).hasNextPage(); - List list = commentsViewModel.getList().getValue(); - list = list != null ? new LinkedList<>(list) : new LinkedList<>(); - // final int oldSize = list != null ? list.size() : 0; - list.addAll(commentModels); - commentsViewModel.getList().postValue(list); - } - binding.swipeRefreshLayout.setRefreshing(false); - stopCurrentExecutor(null); - } - - @Override - public void onFailure(Throwable t) { - stopCurrentExecutor(t); - } - }; - - private final CommentsAdapter.CommentCallback commentCallback = new CommentsAdapter.CommentCallback() { - @Override - public void onClick(final CommentModel comment) { - onCommentClick(comment); - } - - @Override - public void onHashtagClick(final String hashtag) { - final NavDirections action = CommentsViewerFragmentDirections.actionGlobalHashTagFragment(hashtag); - NavHostFragment.findNavController(CommentsViewerFragment.this).navigate(action); - } - - @Override - public void onMentionClick(final String mention) { - openProfile(mention); - } - - @Override - public void onURLClick(final String url) { - Utils.openURL(getContext(), url); - } - - @Override - public void onEmailClick(final String emailAddress) { - Utils.openEmailAddress(getContext(), emailAddress); - } - }; - private final View.OnClickListener newCommentListener = v -> { - final Editable text = binding.commentText.getText(); - final Context context = getContext(); - if (context == null) return; - if (text == null || TextUtils.isEmpty(text.toString())) { - Toast.makeText(context, R.string.comment_send_empty_comment, Toast.LENGTH_SHORT).show(); - return; - } - if (userIdFromCookie == 0) return; - String replyToId = null; - final CommentModel commentModel = commentsAdapter.getSelected(); - if (commentModel != null) { - replyToId = commentModel.getId(); - } - mediaService.comment(postId, text.toString(), replyToId, new ServiceCallback() { - @Override - public void onSuccess(final Boolean result) { - commentsAdapter.clearSelection(); - binding.commentText.setText(""); - if (!result) { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - return; - } - onRefresh(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error during comment", t); - Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - } - }); - }; - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); - final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); - userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie); - mediaService = MediaService.getInstance(deviceUuid, csrfToken, userIdFromCookie); - // setHasOptionsMenu(true); - } - - @NonNull - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { - if (root != null) { - shouldRefresh = false; - return root; - } - binding = FragmentCommentsBinding.inflate(getLayoutInflater()); - binding.swipeRefreshLayout.setEnabled(false); - binding.swipeRefreshLayout.setNestedScrollingEnabled(false); - root = binding.getRoot(); - return root; - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - if (!shouldRefresh) return; - init(); - shouldRefresh = false; - } - - // @Override - // public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { - // inflater.inflate(R.menu.follow, menu); - // menu.findItem(R.id.action_compare).setVisible(false); - // final MenuItem menuSearch = menu.findItem(R.id.action_search); - // final SearchView searchView = (SearchView) menuSearch.getActionView(); - // searchView.setQueryHint(getResources().getString(R.string.action_search)); - // searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - // @Override - // public boolean onQueryTextSubmit(final String query) { - // return false; - // } - // - // @Override - // public boolean onQueryTextChange(final String query) { - // // if (commentsAdapter != null) commentsAdapter.getFilter().filter(query); - // return true; - // } - // }); - // } - - @Override - public void onRefresh() { - endCursor = null; - lazyLoader.resetState(); - commentsViewModel.getList().postValue(Collections.emptyList()); - stopCurrentExecutor(null); - currentlyRunning = new CommentsFetcher(shortCode, "", fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - - private void init() { - if (getArguments() == null) return; - final CommentsViewerFragmentArgs fragmentArgs = CommentsViewerFragmentArgs.fromBundle(getArguments()); - shortCode = fragmentArgs.getShortCode(); - postId = fragmentArgs.getPostId(); - authorUserId = fragmentArgs.getPostUserId(); - // setTitle(); - binding.swipeRefreshLayout.setOnRefreshListener(this); - binding.swipeRefreshLayout.setRefreshing(true); - commentsViewModel = new ViewModelProvider(this).get(CommentsViewModel.class); - final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); - binding.rvComments.setLayoutManager(layoutManager); - commentsAdapter = new CommentsAdapter(commentCallback); - binding.rvComments.setAdapter(commentsAdapter); - commentsViewModel.getList().observe(getViewLifecycleOwner(), commentsAdapter::submitList); - resources = getResources(); - if (!TextUtils.isEmpty(cookie)) { - binding.commentField.setStartIconVisible(false); - binding.commentField.setEndIconVisible(false); - binding.commentField.setVisibility(View.VISIBLE); - binding.commentText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {} - - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { - binding.commentField.setStartIconVisible(s.length() > 0); - binding.commentField.setEndIconVisible(s.length() > 0); - } - - @Override - public void afterTextChanged(final Editable s) {} - }); - binding.commentField.setStartIconOnClickListener(v -> { - commentsAdapter.clearSelection(); - binding.commentText.setText(""); - }); - binding.commentField.setEndIconOnClickListener(newCommentListener); - } - lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { - if (hasNextPage && !TextUtils.isEmpty(endCursor)) - currentlyRunning = new CommentsFetcher(shortCode, endCursor, fetchListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - endCursor = null; - }); - binding.rvComments.addOnScrollListener(lazyLoader); - stopCurrentExecutor(null); - onRefresh(); - } - - // private void setTitle() { - // final ActionBar actionBar = fragmentActivity.getSupportActionBar(); - // if (actionBar == null) return; - // actionBar.setTitle(R.string.title_comments); - // actionBar.setSubtitle(shortCode); - // } - - private void onCommentClick(final CommentModel commentModel) { - final String username = commentModel.getProfileModel().getUsername(); - final SpannableString title = new SpannableString(username + ":\n" + commentModel.getText()); - title.setSpan(new RelativeSizeSpan(1.23f), 0, username.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); - - String[] commentDialogList; - - if (!TextUtils.isEmpty(cookie) - && userIdFromCookie != 0 - && (userIdFromCookie == commentModel.getProfileModel().getPk() || userIdFromCookie == authorUserId)) { - commentDialogList = new String[]{ - resources.getString(R.string.open_profile), - resources.getString(R.string.comment_viewer_copy_comment), - resources.getString(R.string.comment_viewer_see_likers), - resources.getString(R.string.comment_viewer_reply_comment), - commentModel.getLiked() ? resources.getString(R.string.comment_viewer_unlike_comment) - : resources.getString(R.string.comment_viewer_like_comment), - resources.getString(R.string.comment_viewer_translate_comment), - resources.getString(R.string.comment_viewer_delete_comment) - }; - } else if (!TextUtils.isEmpty(cookie)) { - commentDialogList = new String[]{ - resources.getString(R.string.open_profile), - resources.getString(R.string.comment_viewer_copy_comment), - resources.getString(R.string.comment_viewer_see_likers), - resources.getString(R.string.comment_viewer_reply_comment), - commentModel.getLiked() ? resources.getString(R.string.comment_viewer_unlike_comment) - : resources.getString(R.string.comment_viewer_like_comment), - resources.getString(R.string.comment_viewer_translate_comment) - }; - } else { - commentDialogList = new String[]{ - resources.getString(R.string.open_profile), - resources.getString(R.string.comment_viewer_copy_comment), - resources.getString(R.string.comment_viewer_see_likers) - }; - } - final Context context = getContext(); - if (context == null) return; - final DialogInterface.OnClickListener profileDialogListener = (dialog, which) -> { - final User profileModel = commentModel.getProfileModel(); - switch (which) { - case 0: // open profile - openProfile("@" + profileModel.getUsername()); - break; - case 1: // copy comment - Utils.copyText(context, "@" + profileModel.getUsername() + ": " + commentModel.getText()); - break; - case 2: // see comment likers, this is surprisingly available to anons - final NavController navController = getNavController(); - if (navController != null) { - final Bundle bundle = new Bundle(); - bundle.putString("postId", commentModel.getId()); - bundle.putBoolean("isComment", true); - navController.navigate(R.id.action_global_likesViewerFragment, bundle); - } else Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - break; - case 3: // reply to comment - commentsAdapter.setSelected(commentModel); - String mention = "@" + profileModel.getUsername() + " "; - binding.commentText.setText(mention); - binding.commentText.requestFocus(); - binding.commentText.setSelection(mention.length()); - binding.commentText.postDelayed(() -> { - imm = (InputMethodManager) context.getSystemService(INPUT_METHOD_SERVICE); - if (imm == null) return; - imm.showSoftInput(binding.commentText, 0); - }, 200); - break; - case 4: // like/unlike comment - if (!commentModel.getLiked()) { - mediaService.commentLike(commentModel.getId(), new ServiceCallback() { - @Override - public void onSuccess(final Boolean result) { - if (!result) { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - return; - } - commentsAdapter.setLiked(commentModel, true); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error liking comment", t); - try { - Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - } catch (final Throwable ignored) {} - } - }); - return; - } - mediaService.commentUnlike(commentModel.getId(), new ServiceCallback() { - @Override - public void onSuccess(final Boolean result) { - if (!result) { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - return; - } - commentsAdapter.setLiked(commentModel, false); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error unliking comment", t); - try { - Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - } catch (final Throwable ignored) {} - } - }); - break; - case 5: // translate comment - mediaService.translate(commentModel.getId(), "2", new ServiceCallback() { - @Override - public void onSuccess(final String result) { - if (TextUtils.isEmpty(result)) { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - return; - } - new AlertDialog.Builder(context) - .setTitle(username) - .setMessage(result) - .setPositiveButton(R.string.ok, null) - .show(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error translating comment", t); - try { - Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - } catch (final Throwable ignored) {} - } - }); - break; - case 6: // delete comment - if (userIdFromCookie == 0) return; - mediaService.deleteComment( - postId, commentModel.getId(), - new ServiceCallback() { - @Override - public void onSuccess(final Boolean result) { - if (!result) { - Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); - return; - } - onRefresh(); - } - - @Override - public void onFailure(final Throwable t) { - Log.e(TAG, "Error deleting comment", t); - try { - Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); - } catch (final Throwable ignored) {} - } - }); - break; - } - }; - new AlertDialog.Builder(context) - .setTitle(title) - .setItems(commentDialogList, profileDialogListener) - .setNegativeButton(R.string.cancel, null) - .show(); - } - - private void openProfile(final String username) { - final NavDirections action = CommentsViewerFragmentDirections.actionGlobalProfileFragment(username); - NavHostFragment.findNavController(this).navigate(action); - } - - private void stopCurrentExecutor(final Throwable t) { - if (currentlyRunning != null) { - try { - currentlyRunning.cancel(true); - } catch (final Exception e) { - if (BuildConfig.DEBUG) Log.e(TAG, "", e); - } - } - if (t != null) { - try { - Toast.makeText(getContext(), t.getMessage(), Toast.LENGTH_SHORT).show(); - binding.swipeRefreshLayout.setRefreshing(false); - } catch (Throwable ignored) {} - } - } - - @Nullable - private NavController getNavController() { - NavController navController = null; - try { - navController = NavHostFragment.findNavController(this); - } catch (IllegalStateException e) { - Log.e(TAG, "navigateToProfile", e); - } - return navController; - } -} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java index f2c12d91..c1f41568 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java @@ -11,6 +11,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.navigation.fragment.NavHostFragment; +import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; @@ -34,7 +35,7 @@ import awais.instagrabber.webservices.ServiceCallback; import static awais.instagrabber.utils.Utils.settingsHelper; public final class LikesViewerFragment extends BottomSheetDialogFragment implements SwipeRefreshLayout.OnRefreshListener { - private static final String TAG = "LikesViewerFragment"; + private static final String TAG = LikesViewerFragment.class.getSimpleName(); private FragmentLikesBinding binding; private RecyclerLazyLoader lazyLoader; @@ -58,6 +59,7 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme }); binding.rvLikes.setAdapter(likesAdapter); binding.rvLikes.setLayoutManager(new LinearLayoutManager(getContext())); + binding.rvLikes.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL)); binding.swipeRefreshLayout.setRefreshing(false); } @@ -71,7 +73,7 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme } }; - private final ServiceCallback acb = new ServiceCallback() { + private final ServiceCallback anonCb = new ServiceCallback() { @Override public void onSuccess(final GraphQLUserListFetchResponse result) { endCursor = result.getNextMaxId(); @@ -127,7 +129,7 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme public void onRefresh() { if (isComment && !isLoggedIn) { lazyLoader.resetState(); - graphQLService.fetchCommentLikers(postId, null, acb); + graphQLService.fetchCommentLikers(postId, null, anonCb); } else mediaService.fetchLikes(postId, isComment, cb); } @@ -141,9 +143,10 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme if (isComment && !isLoggedIn) { final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); binding.rvLikes.setLayoutManager(layoutManager); + binding.rvLikes.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.HORIZONTAL)); lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { if (!TextUtils.isEmpty(endCursor)) - graphQLService.fetchCommentLikers(postId, endCursor, acb); + graphQLService.fetchCommentLikers(postId, endCursor, anonCb); endCursor = null; }); binding.rvLikes.addOnScrollListener(lazyLoader); diff --git a/app/src/main/java/awais/instagrabber/fragments/comments/CommentsViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/comments/CommentsViewerFragment.java new file mode 100644 index 00000000..aa70ad25 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/comments/CommentsViewerFragment.java @@ -0,0 +1,237 @@ +package awais.instagrabber.fragments.comments; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.RelativeSizeSpan; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavController; +import androidx.navigation.fragment.NavHostFragment; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.snackbar.Snackbar; + +import java.util.Collections; +import java.util.List; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.CommentsAdapter; +import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; +import awais.instagrabber.databinding.FragmentCommentsBinding; +import awais.instagrabber.models.Comment; +import awais.instagrabber.models.Resource; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.viewmodels.AppStateViewModel; +import awais.instagrabber.viewmodels.CommentsViewerViewModel; + +public final class CommentsViewerFragment extends BottomSheetDialogFragment { + private static final String TAG = CommentsViewerFragment.class.getSimpleName(); + + private CommentsViewerViewModel viewModel; + private CommentsAdapter commentsAdapter; + private FragmentCommentsBinding binding; + private ConstraintLayout root; + private boolean shouldRefresh = true; + private AppStateViewModel appStateViewModel; + private boolean showingReplies; + + @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(); + final BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheetInternal); + behavior.setState(BottomSheetBehavior.STATE_EXPANDED); + behavior.setSkipCollapsed(true); + } + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final FragmentActivity activity = getActivity(); + if (activity == null) return; + viewModel = new ViewModelProvider(this).get(CommentsViewerViewModel.class); + appStateViewModel = new ViewModelProvider(activity).get(AppStateViewModel.class); + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { + return new BottomSheetDialog(getContext(), getTheme()) { + @Override + public void onBackPressed() { + if (showingReplies) { + getChildFragmentManager().popBackStack(); + showingReplies = false; + return; + } + super.onBackPressed(); + } + }; + } + + @NonNull + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + if (root != null) { + shouldRefresh = false; + return root; + } + binding = FragmentCommentsBinding.inflate(getLayoutInflater()); + binding.swipeRefreshLayout.setEnabled(false); + binding.swipeRefreshLayout.setNestedScrollingEnabled(false); + root = binding.getRoot(); + appStateViewModel.getCurrentUserLiveData().observe(getViewLifecycleOwner(), user -> viewModel.setCurrentUser(user)); + if (getArguments() == null) return root; + final CommentsViewerFragmentArgs args = CommentsViewerFragmentArgs.fromBundle(getArguments()); + viewModel.setPostDetails(args.getShortCode(), args.getPostId(), args.getPostUserId()); + return root; + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + if (!shouldRefresh) return; + shouldRefresh = false; + init(); + } + + private void init() { + setupToolbar(); + setupList(); + setupObservers(); + } + + private void setupObservers() { + viewModel.getCurrentUserId().observe(getViewLifecycleOwner(), currentUserId -> { + long userId = 0; + if (currentUserId != null) { + userId = currentUserId; + } + setupAdapter(userId); + if (userId == 0) return; + Helper.setupCommentInput(binding.commentField, binding.commentText, false, text -> { + final LiveData> resourceLiveData = viewModel.comment(text, false); + resourceLiveData.observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(final Resource objectResource) { + if (objectResource == null) return; + final Context context = getContext(); + if (context == null) return; + Helper.handleCommentResource( + context, + objectResource.status, + objectResource.message, + resourceLiveData, + this, + binding.commentField, + binding.commentText, + binding.comments); + } + }); + return null; + }); + }); + viewModel.getRootList().observe(getViewLifecycleOwner(), listResource -> { + if (listResource == null) return; + switch (listResource.status) { + case SUCCESS: + binding.swipeRefreshLayout.setRefreshing(false); + if (commentsAdapter != null) { + commentsAdapter.submitList(listResource.data); + } + break; + case ERROR: + binding.swipeRefreshLayout.setRefreshing(false); + if (!TextUtils.isEmpty(listResource.message)) { + Snackbar.make(binding.getRoot(), listResource.message, Snackbar.LENGTH_LONG).show(); + } + break; + case LOADING: + binding.swipeRefreshLayout.setRefreshing(true); + break; + } + }); + viewModel.getRootCommentsCount().observe(getViewLifecycleOwner(), count -> { + if (count == null || count == 0) { + binding.toolbar.setTitle(R.string.title_comments); + return; + } + final String titleComments = getString(R.string.title_comments); + final String countString = String.valueOf(count); + final SpannableString titleWithCount = new SpannableString(String.format("%s %s", titleComments, countString)); + titleWithCount.setSpan(new RelativeSizeSpan(0.8f), + titleWithCount.length() - countString.length(), + titleWithCount.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + binding.toolbar.setTitle(titleWithCount); + }); + } + + private void setupToolbar() { + binding.toolbar.setTitle(R.string.title_comments); + } + + private void setupAdapter(final long currentUserId) { + final Context context = getContext(); + if (context == null) return; + commentsAdapter = new CommentsAdapter(currentUserId, false, Helper.getCommentCallback( + context, + getViewLifecycleOwner(), + getNavController(), + viewModel, + (comment, focusInput) -> { + if (comment == null) return null; + final RepliesFragment repliesFragment = RepliesFragment.newInstance(comment, focusInput == null ? false : focusInput); + getChildFragmentManager().beginTransaction() + .setCustomAnimations(R.anim.slide_left, R.anim.slide_right, 0, R.anim.slide_right) + .add(R.id.replies_container_view, repliesFragment) + .addToBackStack(RepliesFragment.TAG) + .commit(); + showingReplies = true; + return null; + })); + final Resource> listResource = viewModel.getRootList().getValue(); + binding.comments.setAdapter(commentsAdapter); + commentsAdapter.submitList(listResource != null ? listResource.data : Collections.emptyList()); + } + + private void setupList() { + final Context context = getContext(); + if (context == null) return; + final LinearLayoutManager layoutManager = new LinearLayoutManager(context); + final RecyclerLazyLoader lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> viewModel.fetchComments()); + Helper.setupList(context, binding.comments, layoutManager, lazyLoader); + } + + @Nullable + private NavController getNavController() { + NavController navController = null; + try { + navController = NavHostFragment.findNavController(this); + } catch (IllegalStateException e) { + Log.e(TAG, "navigateToProfile", e); + } + return navController; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/comments/Helper.java b/app/src/main/java/awais/instagrabber/fragments/comments/Helper.java new file mode 100644 index 00000000..823ffb33 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/comments/Helper.java @@ -0,0 +1,288 @@ +package awais.instagrabber.fragments.comments; + +import android.content.Context; +import android.os.Bundle; +import android.text.Editable; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.navigation.NavController; +import androidx.navigation.NavDirections; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.internal.CheckableImageButton; +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; + +import java.util.function.BiFunction; +import java.util.function.Function; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.CommentsAdapter.CommentCallback; +import awais.instagrabber.customviews.helpers.TextWatcherAdapter; +import awais.instagrabber.models.Comment; +import awais.instagrabber.models.Resource; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; +import awais.instagrabber.viewmodels.CommentsViewerViewModel; +import awais.instagrabber.webservices.ServiceCallback; + +public final class Helper { + private static final String TAG = Helper.class.getSimpleName(); + + public static void setupList(@NonNull final Context context, + @NonNull final RecyclerView list, + @NonNull final RecyclerView.LayoutManager layoutManager, + @NonNull final RecyclerView.OnScrollListener lazyLoader) { + list.setLayoutManager(layoutManager); + final DividerItemDecoration itemDecoration = new DividerItemDecoration(context, LinearLayoutManager.VERTICAL); + itemDecoration.setDrawable(ContextCompat.getDrawable(context, R.drawable.pref_list_divider_material)); + list.addItemDecoration(itemDecoration); + list.addOnScrollListener(lazyLoader); + } + + @NonNull + public static CommentCallback getCommentCallback(@NonNull final Context context, + final LifecycleOwner lifecycleOwner, + final NavController navController, + @NonNull final CommentsViewerViewModel viewModel, + final BiFunction onRepliesClick) { + return new CommentCallback() { + @Override + public void onClick(final Comment comment) { + // onCommentClick(comment); + if (onRepliesClick == null) return; + onRepliesClick.apply(comment, false); + } + + @Override + public void onHashtagClick(final String hashtag) { + try { + if (navController == null) return; + final NavDirections action = CommentsViewerFragmentDirections.actionGlobalHashTagFragment(hashtag); + navController.navigate(action); + } catch (Exception e) { + Log.e(TAG, "onHashtagClick: ", e); + } + } + + @Override + public void onMentionClick(final String mention) { + openProfile(navController, mention); + } + + @Override + public void onURLClick(final String url) { + Utils.openURL(context, url); + } + + @Override + public void onEmailClick(final String emailAddress) { + Utils.openEmailAddress(context, emailAddress); + } + + @Override + public void onLikeClick(final Comment comment, final boolean liked, final boolean isReply) { + if (comment == null) return; + final LiveData> resourceLiveData = viewModel.likeComment(comment, liked, isReply); + resourceLiveData.observe(lifecycleOwner, new Observer>() { + @Override + public void onChanged(final Resource objectResource) { + if (objectResource == null) return; + switch (objectResource.status) { + case SUCCESS: + resourceLiveData.removeObserver(this); + break; + case LOADING: + break; + case ERROR: + if (objectResource.message != null) { + Toast.makeText(context, objectResource.message, Toast.LENGTH_LONG).show(); + } + resourceLiveData.removeObserver(this); + } + } + }); + } + + @Override + public void onRepliesClick(final Comment comment) { + // viewModel.showReplies(comment); + if (onRepliesClick == null) return; + onRepliesClick.apply(comment, true); + } + + @Override + public void onViewLikes(final Comment comment) { + if (navController == null) return; + try { + final Bundle bundle = new Bundle(); + bundle.putString("postId", comment.getId()); + bundle.putBoolean("isComment", true); + navController.navigate(R.id.action_global_likesViewerFragment, bundle); + } catch (Exception e) { + Log.e(TAG, "onViewLikes: ", e); + } + } + + @Override + public void onTranslate(final Comment comment) { + if (comment == null) return; + viewModel.translate(comment, new ServiceCallback() { + @Override + public void onSuccess(final String result) { + if (TextUtils.isEmpty(result)) { + Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); + return; + } + String username = ""; + if (comment.getUser() != null) { + username = comment.getUser().getUsername(); + } + new MaterialAlertDialogBuilder(context) + .setTitle(username) + .setMessage(result) + .setPositiveButton(R.string.ok, null) + .show(); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error translating comment", t); + Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + + @Override + public void onDelete(final Comment comment, final boolean isReply) { + if (comment == null) return; + final LiveData> resourceLiveData = viewModel.deleteComment(comment, isReply); + resourceLiveData.observe(lifecycleOwner, new Observer>() { + @Override + public void onChanged(final Resource objectResource) { + if (objectResource == null) return; + switch (objectResource.status) { + case SUCCESS: + resourceLiveData.removeObserver(this); + break; + case ERROR: + if (objectResource.message != null) { + Toast.makeText(context, objectResource.message, Toast.LENGTH_LONG).show(); + } + resourceLiveData.removeObserver(this); + break; + case LOADING: + break; + } + } + }); + } + }; + } + + private static void openProfile(final NavController navController, + @NonNull final String username) { + if (navController == null) return; + try { + final NavDirections action = CommentsViewerFragmentDirections.actionGlobalProfileFragment(username); + navController.navigate(action); + } catch (Exception e) { + Log.e(TAG, "openProfile: ", e); + } + } + + public static void setupCommentInput(@NonNull final TextInputLayout commentField, + @NonNull final TextInputEditText commentText, + final boolean isReplyFragment, + @NonNull final Function commentFunction) { + // commentField.setStartIconVisible(false); + commentField.setVisibility(View.VISIBLE); + commentField.setEndIconVisible(false); + if (isReplyFragment) { + commentField.setHint(R.string.reply_hint); + } + commentText.addTextChangedListener(new TextWatcherAdapter() { + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + final boolean isEmpty = TextUtils.isEmpty(s); + commentField.setStartIconVisible(!isEmpty); + commentField.setEndIconVisible(!isEmpty); + commentField.setCounterEnabled(s != null && s.length() > 2000); // show the counter when user approaches the limit + } + }); + // commentField.setStartIconOnClickListener(v -> { + // // commentsAdapter.clearSelection(); + // commentText.setText(""); + // }); + commentField.setEndIconOnClickListener(v -> { + final Editable text = commentText.getText(); + if (TextUtils.isEmpty(text)) return; + commentFunction.apply(text.toString().trim()); + }); + } + + public static void handleCommentResource(@NonNull final Context context, + @NonNull final Resource.Status status, + final String message, + @NonNull final LiveData> resourceLiveData, + @NonNull final Observer> observer, + @NonNull final TextInputLayout commentField, + @NonNull final TextInputEditText commentText, + @NonNull final RecyclerView comments) { + CheckableImageButton endIcon = null; + try { + endIcon = (CheckableImageButton) commentField.findViewById(com.google.android.material.R.id.text_input_end_icon); + } catch (Exception e) { + Log.e(TAG, "setupObservers: ", e); + } + CheckableImageButton startIcon = null; + try { + startIcon = (CheckableImageButton) commentField.findViewById(com.google.android.material.R.id.text_input_start_icon); + } catch (Exception e) { + Log.e(TAG, "setupObservers: ", e); + } + switch (status) { + case SUCCESS: + resourceLiveData.removeObserver(observer); + comments.postDelayed(() -> comments.scrollToPosition(0), 500); + if (startIcon != null) { + startIcon.setEnabled(true); + } + if (endIcon != null) { + endIcon.setEnabled(true); + } + commentText.setText(""); + break; + case LOADING: + commentText.setEnabled(false); + if (startIcon != null) { + startIcon.setEnabled(false); + } + if (endIcon != null) { + endIcon.setEnabled(false); + } + break; + case ERROR: + if (message != null && context != null) { + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + } + if (startIcon != null) { + startIcon.setEnabled(true); + } + if (endIcon != null) { + endIcon.setEnabled(true); + } + resourceLiveData.removeObserver(observer); + } + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/comments/RepliesFragment.java b/app/src/main/java/awais/instagrabber/fragments/comments/RepliesFragment.java new file mode 100644 index 00000000..67c0f95c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/comments/RepliesFragment.java @@ -0,0 +1,214 @@ +package awais.instagrabber.fragments.comments; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavController; +import androidx.navigation.fragment.NavHostFragment; +import androidx.recyclerview.widget.LinearLayoutManager; + +import com.google.android.material.snackbar.Snackbar; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.CommentsAdapter; +import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; +import awais.instagrabber.databinding.FragmentCommentsBinding; +import awais.instagrabber.models.Comment; +import awais.instagrabber.models.Resource; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.utils.Utils; +import awais.instagrabber.viewmodels.CommentsViewerViewModel; + +public class RepliesFragment extends Fragment { + public static final String TAG = RepliesFragment.class.getSimpleName(); + private static final String ARG_PARENT = "parent"; + private static final String ARG_FOCUS_INPUT = "focus"; + + private FragmentCommentsBinding binding; + private CommentsViewerViewModel viewModel; + private CommentsAdapter commentsAdapter; + + @NonNull + public static RepliesFragment newInstance(@NonNull final Comment parent, + final boolean focusInput) { + final Bundle args = new Bundle(); + args.putSerializable(ARG_PARENT, parent); + args.putBoolean(ARG_FOCUS_INPUT, focusInput); + final RepliesFragment fragment = new RepliesFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + viewModel = new ViewModelProvider(getParentFragment()).get(CommentsViewerViewModel.class); + final Bundle bundle = getArguments(); + if (bundle == null) return; + final Serializable serializable = bundle.getSerializable(ARG_PARENT); + if (!(serializable instanceof Comment)) return; + viewModel.showReplies((Comment) serializable); + } + + @Nullable + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + binding = FragmentCommentsBinding.inflate(inflater, container, false); + binding.swipeRefreshLayout.setEnabled(false); + binding.swipeRefreshLayout.setNestedScrollingEnabled(false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + setupToolbar(); + } + + @Override + public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { + if (!enter || nextAnim == 0) { + return super.onCreateAnimation(transit, enter, nextAnim); + } + final Animation animation = AnimationUtils.loadAnimation(getContext(), nextAnim); + animation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} + + @Override + public void onAnimationEnd(Animation animation) { + setupList(); + setupObservers(); + } + + @Override + public void onAnimationRepeat(Animation animation) {} + }); + return animation; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } + + @Override + public void onDestroy() { + super.onDestroy(); + viewModel.clearReplies(); + } + + private void setupObservers() { + viewModel.getCurrentUserId().observe(getViewLifecycleOwner(), currentUserId -> { + long userId = 0; + if (currentUserId != null) { + userId = currentUserId; + } + setupAdapter(userId); + if (userId == 0) return; + Helper.setupCommentInput(binding.commentField, binding.commentText, true, text -> { + final LiveData> resourceLiveData = viewModel.comment(text, true); + resourceLiveData.observe(getViewLifecycleOwner(), new Observer>() { + @Override + public void onChanged(final Resource objectResource) { + if (objectResource == null) return; + final Context context = getContext(); + if (context == null) return; + Helper.handleCommentResource(context, + objectResource.status, + objectResource.message, + resourceLiveData, + this, + binding.commentField, + binding.commentText, + binding.comments); + } + }); + return null; + }); + final Bundle bundle = getArguments(); + if (bundle == null) return; + final boolean focusInput = bundle.getBoolean(ARG_FOCUS_INPUT); + if (focusInput && viewModel.getRepliesParent() != null && viewModel.getRepliesParent().getUser() != null) { + binding.commentText.setText(String.format("@%s ", viewModel.getRepliesParent().getUser().getUsername())); + Utils.showKeyboard(binding.commentText); + } + }); + viewModel.getReplyList().observe(getViewLifecycleOwner(), listResource -> { + if (listResource == null) return; + switch (listResource.status) { + case SUCCESS: + binding.swipeRefreshLayout.setRefreshing(false); + if (commentsAdapter != null) { + commentsAdapter.submitList(listResource.data); + } + break; + case ERROR: + binding.swipeRefreshLayout.setRefreshing(false); + if (!TextUtils.isEmpty(listResource.message)) { + Snackbar.make(binding.getRoot(), listResource.message, Snackbar.LENGTH_LONG).show(); + } + break; + case LOADING: + binding.swipeRefreshLayout.setRefreshing(true); + break; + } + }); + } + + private void setupToolbar() { + binding.toolbar.setTitle("Replies"); + binding.toolbar.setNavigationIcon(R.drawable.ic_round_arrow_back_24); + binding.toolbar.setNavigationOnClickListener(v -> { + final FragmentManager fragmentManager = getParentFragmentManager(); + fragmentManager.popBackStack(); + }); + } + + private void setupAdapter(final long currentUserId) { + final Context context = getContext(); + if (context == null) return; + commentsAdapter = new CommentsAdapter(currentUserId, + true, + Helper.getCommentCallback(context, getViewLifecycleOwner(), getNavController(), viewModel, null)); + binding.comments.setAdapter(commentsAdapter); + final Resource> listResource = viewModel.getReplyList().getValue(); + commentsAdapter.submitList(listResource != null ? listResource.data : Collections.emptyList()); + } + + private void setupList() { + final Context context = getContext(); + if (context == null) return; + final LinearLayoutManager layoutManager = new LinearLayoutManager(context); + final RecyclerLazyLoader lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> viewModel.fetchReplies()); + Helper.setupList(context, binding.comments, layoutManager, lazyLoader); + } + + @Nullable + private NavController getNavController() { + NavController navController = null; + try { + navController = NavHostFragment.findNavController(this); + } catch (IllegalStateException e) { + Log.e(TAG, "navigateToProfile", e); + } + return navController; + } +} 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 73bf0244..fde7950a 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -514,7 +514,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact super.onDestroy(); } - @SuppressLint("UnsafeExperimentalUsageError") + @SuppressLint("UnsafeOptInUsageError") private void cleanup() { if (prevTitleRunnable != null) { appExecutors.mainThread().cancel(prevTitleRunnable); @@ -840,7 +840,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } } - @SuppressLint("UnsafeExperimentalUsageError") + @SuppressLint("UnsafeOptInUsageError") private void attachPendingRequestsBadge(@Nullable final Integer count) { if (pendingRequestCountBadgeDrawable == null) { final Context context = getContext(); diff --git a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java index 9baaded8..0ac6e566 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java @@ -111,12 +111,16 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre @Override public void onCommentsClick(final Media feedModel) { - final NavDirections commentsAction = FeedFragmentDirections.actionGlobalCommentsViewerFragment( - feedModel.getCode(), - feedModel.getPk(), - feedModel.getUser().getPk() - ); - NavHostFragment.findNavController(FeedFragment.this).navigate(commentsAction); + try { + final NavDirections commentsAction = FeedFragmentDirections.actionGlobalCommentsViewerFragment( + feedModel.getCode(), + feedModel.getPk(), + feedModel.getUser().getPk() + ); + NavHostFragment.findNavController(FeedFragment.this).navigate(commentsAction); + } catch (Exception e) { + Log.e(TAG, "onCommentsClick: ", e); + } } @Override diff --git a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java index 443178c5..75802ac0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java @@ -188,7 +188,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe @Override public void onCommentsClick(final Media feedModel) { - final NavDirections commentsAction = FeedFragmentDirections.actionGlobalCommentsViewerFragment( + final NavDirections commentsAction = ProfileFragmentDirections.actionGlobalCommentsViewerFragment( feedModel.getCode(), feedModel.getPk(), feedModel.getUser().getPk() @@ -991,7 +991,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe if (chainingMenuItem != null) { chainingMenuItem.setVisible(true); } - return; } } diff --git a/app/src/main/java/awais/instagrabber/models/Comment.java b/app/src/main/java/awais/instagrabber/models/Comment.java new file mode 100644 index 00000000..d8ff3cb5 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/models/Comment.java @@ -0,0 +1,117 @@ +package awais.instagrabber.models; + +import androidx.annotation.NonNull; + +import java.io.Serializable; +import java.util.Date; +import java.util.Objects; + +import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.utils.Utils; + +public class Comment implements Serializable, Cloneable { + private final User user; + private final String id; + private final String text; + private long likes; + private final long timestamp; + private boolean liked; + private final int replyCount; + private final boolean isChild; + + public Comment(final String id, + final String text, + final long timestamp, + final long likes, + final boolean liked, + final User user, + final int replyCount, final boolean isChild) { + this.id = id; + this.text = text; + this.likes = likes; + this.liked = liked; + this.timestamp = timestamp; + this.user = user; + this.replyCount = replyCount; + this.isChild = isChild; + } + + public String getId() { + return id; + } + + public String getText() { + return text; + } + + @NonNull + public String getDateTime() { + return Utils.datetimeParser.format(new Date(timestamp * 1000L)); + } + + public long getLikes() { + return likes; + } + + public boolean getLiked() { + return liked; + } + + public void setLiked(boolean liked) { + this.likes = liked ? likes + 1 : likes - 1; + this.liked = liked; + } + + public User getUser() { + return user; + } + + public int getReplyCount() { + return replyCount; + } + + public boolean isChild() { + return isChild; + } + + @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 Comment comment = (Comment) o; + return likes == comment.likes && + timestamp == comment.timestamp && + liked == comment.liked && + replyCount == comment.replyCount && + Objects.equals(user, comment.user) && + Objects.equals(id, comment.id) && + Objects.equals(text, comment.text) && + isChild == comment.isChild; + } + + @Override + public int hashCode() { + return Objects.hash(user, id, text, likes, timestamp, liked, replyCount, isChild); + } + + @NonNull + @Override + public String toString() { + return "Comment{" + + "user=" + user + + ", id='" + id + '\'' + + ", text='" + text + '\'' + + ", likes=" + likes + + ", timestamp=" + timestamp + + ", liked=" + liked + + ", replyCount" + replyCount + + ", isChild" + isChild + + '}'; + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/models/CommentModel.java b/app/src/main/java/awais/instagrabber/models/CommentModel.java deleted file mode 100755 index 3331cf87..00000000 --- a/app/src/main/java/awais/instagrabber/models/CommentModel.java +++ /dev/null @@ -1,103 +0,0 @@ -package awais.instagrabber.models; - -import androidx.annotation.NonNull; - -import java.util.Date; -import java.util.List; - -import awais.instagrabber.repositories.responses.User; -import awais.instagrabber.utils.Utils; - -public class CommentModel { - private final User profileModel; - private final String id; - private final String text; - private long likes; - private final long timestamp; - private List childCommentModels; - private boolean liked, hasNextPage; - private String endCursor; - - public CommentModel(final String id, - final String text, - final long timestamp, - final long likes, - final boolean liked, - final User profileModel) { - this.id = id; - this.text = text; - this.likes = likes; - this.liked = liked; - this.timestamp = timestamp; - this.profileModel = profileModel; - } - - public String getId() { - return id; - } - - public String getText() { - return text; - } - - @NonNull - public String getDateTime() { - return Utils.datetimeParser.format(new Date(timestamp * 1000L)); - } - - public long getLikes() { - return likes; - } - - public boolean getLiked() { - return liked; - } - - public void setLiked(boolean liked) { - this.likes = liked ? likes + 1 : likes - 1; - this.liked = liked; - } - - public User getProfileModel() { - return profileModel; - } - - public List getChildCommentModels() { - return childCommentModels; - } - - public void setChildCommentModels(final List childCommentModels) { - this.childCommentModels = childCommentModels; - } - - public void setPageCursor(final boolean hasNextPage, final String endCursor) { - this.hasNextPage = hasNextPage; - this.endCursor = endCursor; - } - - public boolean hasNextPage() { - return hasNextPage; - } - - public String getEndCursor() { - return endCursor; - } - - // @NonNull - // @Override - // public String toString() { - // try { - // final JSONObject object = new JSONObject(); - // object.put(Constants.EXTRAS_ID, id); - // object.put("text", text); - // object.put(Constants.EXTRAS_NAME, profileModel != null ? profileModel.getUsername() : ""); - // if (childCommentModels != null) object.put("childComments", childCommentModels); - // return object.toString(); - // } catch (Exception e) { - // return "{\"id\":\"" + id + "\", \"text\":\"" + text - // //(text != null ? text.replaceAll("\"", "\\\\\"") : "") - // + "\", \"name\":\"" + (profileModel != null ? profileModel.getUsername() : "") + - // (childCommentModels != null ? "\", \"childComments\":" + childCommentModels.length : "\"") + '}'; - // } - // } -} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/GraphQLCommentsFetchResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/GraphQLCommentsFetchResponse.java new file mode 100644 index 00000000..22c3c7b9 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/GraphQLCommentsFetchResponse.java @@ -0,0 +1,51 @@ +package awais.instagrabber.repositories.responses; + +import androidx.annotation.NonNull; + +import java.util.List; + +import awais.instagrabber.models.Comment; + +public class GraphQLCommentsFetchResponse { + private final int count; + private final String cursor; + private final boolean hasNext; + private final List comments; + + public GraphQLCommentsFetchResponse(final int count, + final String cursor, + final boolean hasNext, + final List comments) { + this.count = count; + this.cursor = cursor; + this.hasNext = hasNext; + this.comments = comments; + } + + public int getCount() { + return count; + } + + public String getCursor() { + return cursor; + } + + public boolean hasNext() { + return hasNext; + } + + public List getComments() { + return comments; + } + + @NonNull + @Override + public String toString() { + return "GraphQLCommentsFetchResponse{" + + "count=" + count + + ", cursor='" + cursor + '\'' + + ", hasNext=" + hasNext + + ", comments=" + comments + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/utils/Utils.java b/app/src/main/java/awais/instagrabber/utils/Utils.java index beb85386..432a0e90 100644 --- a/app/src/main/java/awais/instagrabber/utils/Utils.java +++ b/app/src/main/java/awais/instagrabber/utils/Utils.java @@ -347,6 +347,18 @@ public final class Utils { ); } + public static void showKeyboard(@NonNull final View view) { + final Context context = view.getContext(); + if (context == null) return; + final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) return; + view.requestFocus(); + final boolean shown = imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + if (!shown) { + Log.e(TAG, "showKeyboard: System did not display the keyboard"); + } + } + public static void hideKeyboard(final View view) { if (view == null) return; final Context context = view.getContext(); diff --git a/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java index 84fd2ba9..5b5bb54e 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java @@ -4,6 +4,7 @@ import android.app.Application; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -36,6 +37,7 @@ public class AppStateViewModel extends AndroidViewModel { fetchProfileDetails(); } + @Nullable public User getCurrentUser() { return currentUser.getValue(); } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewModel.java deleted file mode 100644 index 7b693ea0..00000000 --- a/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewModel.java +++ /dev/null @@ -1,19 +0,0 @@ -package awais.instagrabber.viewmodels; - -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.ViewModel; - -import java.util.List; - -import awais.instagrabber.models.CommentModel; - -public class CommentsViewModel extends ViewModel { - private MutableLiveData> list; - - public MutableLiveData> getList() { - if (list == null) { - list = new MutableLiveData<>(); - } - return list; - } -} diff --git a/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java new file mode 100644 index 00000000..109b6fea --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java @@ -0,0 +1,447 @@ +package awais.instagrabber.viewmodels; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.google.common.collect.ImmutableList; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.OptionalInt; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import awais.instagrabber.R; +import awais.instagrabber.models.Comment; +import awais.instagrabber.models.Resource; +import awais.instagrabber.repositories.responses.FriendshipStatus; +import awais.instagrabber.repositories.responses.GraphQLCommentsFetchResponse; +import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; +import awais.instagrabber.utils.Utils; +import awais.instagrabber.webservices.GraphQLService; +import awais.instagrabber.webservices.MediaService; +import awais.instagrabber.webservices.ServiceCallback; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import static awais.instagrabber.utils.Utils.settingsHelper; + +public class CommentsViewerViewModel extends ViewModel { + private static final String TAG = CommentsViewerViewModel.class.getSimpleName(); + + private final MutableLiveData isLoggedIn = new MutableLiveData<>(false); + private final MutableLiveData currentUserId = new MutableLiveData<>(0L); + private final MutableLiveData>> rootList = new MutableLiveData<>(); + private final MutableLiveData rootCount = new MutableLiveData<>(0); + private final MutableLiveData>> replyList = new MutableLiveData<>(); + private final GraphQLService service; + + private String shortCode; + private String postId; + private String rootCursor; + private boolean rootHasNext = true; + private Comment repliesParent; + private String repliesCursor; + private boolean repliesHasNext = true; + private final MediaService mediaService; + private List prevReplies; + private String prevRepliesCursor; + private boolean prevRepliesHasNext = true; + + public CommentsViewerViewModel() { + service = GraphQLService.getInstance(); + final String cookie = settingsHelper.getString(Constants.COOKIE); + final String deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID); + final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); + final long userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie); + mediaService = MediaService.getInstance(deviceUuid, csrfToken, userIdFromCookie); + } + + public void setCurrentUser(final User currentUser) { + isLoggedIn.postValue(currentUser != null); + currentUserId.postValue(currentUser == null ? 0 : currentUser.getPk()); + } + + public void setPostDetails(final String shortCode, final String postId, final long postUserId) { + this.shortCode = shortCode; + this.postId = postId; + fetchComments(); + } + + public LiveData isLoggedIn() { + return isLoggedIn; + } + + public LiveData getCurrentUserId() { + return currentUserId; + } + + @Nullable + public Comment getRepliesParent() { + return repliesParent; + } + + public LiveData>> getRootList() { + return rootList; + } + + public LiveData>> getReplyList() { + return replyList; + } + + public LiveData getRootCommentsCount() { + return rootCount; + } + + public void fetchComments() { + if (shortCode == null) return; + fetchComments(shortCode, true); + } + + public void fetchReplies() { + if (repliesParent == null) return; + fetchReplies(repliesParent.getId()); + } + + public void fetchReplies(@NonNull final String commentId) { + fetchComments(commentId, false); + } + + public void fetchComments(@NonNull final String shortCodeOrCommentId, + final boolean root) { + if (root) { + if (!rootHasNext) return; + rootList.postValue(Resource.loading(getPrevList(rootList))); + } else { + if (!repliesHasNext) return; + final List list; + if (repliesParent != null && !Objects.equals(repliesParent.getId(), shortCodeOrCommentId)) { + repliesCursor = null; + repliesHasNext = false; + list = Collections.emptyList(); + } else { + list = getPrevList(replyList); + } + replyList.postValue(Resource.loading(list)); + } + final Call request = service.fetchComments(shortCodeOrCommentId, root, root ? rootCursor : repliesCursor); + enqueueRequest(request, root, shortCodeOrCommentId, new ServiceCallback() { + @Override + public void onSuccess(final GraphQLCommentsFetchResponse result) { + // Log.d(TAG, "onSuccess: " + result); + List comments = result.getComments(); + if (root) { + if (rootCursor == null) { + rootCount.postValue(result.getCount()); + } + if (rootCursor != null) { + comments = mergeList(rootList, comments); + } + rootCursor = result.getCursor(); + rootHasNext = result.hasNext(); + rootList.postValue(Resource.success(comments)); + return; + } + // Replies + if (repliesCursor == null) { + // add parent to top of replies + comments = ImmutableList.builder() + .add(repliesParent) + .addAll(comments) + .build(); + } + if (repliesCursor != null) { + comments = mergeList(replyList, comments); + } + repliesCursor = result.getCursor(); + repliesHasNext = result.hasNext(); + replyList.postValue(Resource.success(comments)); + + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "onFailure: ", t); + if (root) { + rootList.postValue(Resource.error(t.getMessage(), getPrevList(rootList))); + return; + } + replyList.postValue(Resource.error(t.getMessage(), getPrevList(replyList))); + } + }); + } + + private void enqueueRequest(@NonNull final Call request, + final boolean root, + final String shortCodeOrCommentId, + final ServiceCallback callback) { + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + final String rawBody = response.body(); + if (rawBody == null) { + Log.e(TAG, "Error occurred while fetching gql comments of " + shortCodeOrCommentId); + callback.onSuccess(null); + return; + } + try { + final JSONObject body = root ? new JSONObject(rawBody).getJSONObject("data") + .getJSONObject("shortcode_media") + .getJSONObject("edge_media_to_parent_comment") + : new JSONObject(rawBody).getJSONObject("data") + .getJSONObject("comment") + .getJSONObject("edge_threaded_comments"); + final int count = body.optInt("count"); + final JSONObject pageInfo = body.getJSONObject("page_info"); + final boolean hasNextPage = pageInfo.getBoolean("has_next_page"); + final String endCursor = pageInfo.isNull("end_cursor") ? null : pageInfo.optString("end_cursor"); + final JSONArray commentsJsonArray = body.getJSONArray("edges"); + final ImmutableList.Builder builder = ImmutableList.builder(); + for (int i = 0; i < commentsJsonArray.length(); i++) { + final Comment commentModel = getComment(commentsJsonArray.getJSONObject(i).getJSONObject("node"), root); + builder.add(commentModel); + } + callback.onSuccess(new GraphQLCommentsFetchResponse(count, endCursor, hasNextPage, builder.build())); + } catch (Exception e) { + Log.e(TAG, "onResponse", e); + callback.onFailure(e); + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + callback.onFailure(t); + } + }); + } + + @NonNull + private Comment getComment(@NonNull final JSONObject commentJsonObject, final boolean root) throws JSONException { + final JSONObject owner = commentJsonObject.getJSONObject("owner"); + final User user = new User( + owner.optLong(Constants.EXTRAS_ID, 0), + owner.getString(Constants.EXTRAS_USERNAME), + null, + false, + owner.getString("profile_pic_url"), + null, + new FriendshipStatus(false, false, false, false, false, false, false, false, false, false), + owner.optBoolean("is_verified"), + false, false, false, false, null, null, 0, 0, 0, 0, null, null, 0, null, null, null, null, + null, null); + final JSONObject likedBy = commentJsonObject.optJSONObject("edge_liked_by"); + final String commentId = commentJsonObject.getString("id"); + final JSONObject childCommentsJsonObject = commentJsonObject.optJSONObject("edge_threaded_comments"); + int replyCount = 0; + if (childCommentsJsonObject != null) { + replyCount = childCommentsJsonObject.optInt("count"); + } + return new Comment(commentId, + commentJsonObject.getString("text"), + commentJsonObject.getLong("created_at"), + likedBy != null ? likedBy.optLong("count", 0) : 0, + commentJsonObject.getBoolean("viewer_has_liked"), + user, + replyCount, + !root); + } + + @NonNull + private List getPrevList(@NonNull final LiveData>> list) { + if (list.getValue() == null) return Collections.emptyList(); + final Resource> listResource = list.getValue(); + if (listResource.data == null) return Collections.emptyList(); + return listResource.data; + } + + private List mergeList(@NonNull final LiveData>> list, + final List comments) { + final List prevList = getPrevList(list); + if (comments == null) { + return prevList; + } + return ImmutableList.builder() + .addAll(prevList) + .addAll(comments) + .build(); + } + + public void showReplies(final Comment comment) { + if (comment == null) return; + if (repliesParent == null || !Objects.equals(repliesParent.getId(), comment.getId())) { + repliesParent = comment; + prevReplies = null; + prevRepliesCursor = null; + prevRepliesHasNext = true; + fetchReplies(comment.getId()); + return; + } + if (prevReplies != null && !prevReplies.isEmpty()) { + // user clicked same comment, show prev loaded replies + repliesCursor = prevRepliesCursor; + repliesHasNext = prevRepliesHasNext; + replyList.postValue(Resource.success(prevReplies)); + return; + } + // prev list was null or empty, fetch + prevRepliesCursor = null; + prevRepliesHasNext = true; + fetchReplies(comment.getId()); + } + + public LiveData> likeComment(@NonNull final Comment comment, final boolean liked, final boolean isReply) { + final MutableLiveData> data = new MutableLiveData<>(Resource.loading(null)); + final ServiceCallback callback = new ServiceCallback() { + @Override + public void onSuccess(final Boolean result) { + if (result == null || !result) { + data.postValue(Resource.error(R.string.downloader_unknown_error, null)); + return; + } + data.postValue(Resource.success(new Object())); + setLiked(isReply, comment, liked); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error liking comment", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }; + if (liked) { + mediaService.commentLike(comment.getId(), callback); + } else { + mediaService.commentUnlike(comment.getId(), callback); + } + return data; + } + + private void setLiked(final boolean isReply, + @NonNull final Comment comment, + final boolean liked) { + final List list = getPrevList(isReply ? replyList : rootList); + if (list == null) return; + final List copy = new ArrayList<>(list); + OptionalInt indexOpt = IntStream.range(0, copy.size()) + .filter(i -> copy.get(i) != null && Objects.equals(copy.get(i).getId(), comment.getId())) + .findFirst(); + if (!indexOpt.isPresent()) return; + try { + final Comment clone = (Comment) comment.clone(); + clone.setLiked(liked); + copy.set(indexOpt.getAsInt(), clone); + final MutableLiveData>> liveData = isReply ? replyList : rootList; + liveData.postValue(Resource.success(copy)); + } catch (Exception e) { + Log.e(TAG, "setLiked: ", e); + } + } + + public LiveData> comment(@NonNull final String text, + final boolean isReply) { + final MutableLiveData> data = new MutableLiveData<>(Resource.loading(null)); + String replyToId = null; + if (isReply && repliesParent != null) { + replyToId = repliesParent.getId(); + } + if (isReply && replyToId == null) { + data.postValue(Resource.error(null, null)); + return data; + } + mediaService.comment(postId, text, replyToId, new ServiceCallback() { + @Override + public void onSuccess(final Comment result) { + if (result == null) { + data.postValue(Resource.error(R.string.downloader_unknown_error, null)); + return; + } + addComment(result, isReply); + data.postValue(Resource.success(new Object())); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error during comment", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + private void addComment(@NonNull final Comment comment, final boolean isReply) { + final List list = getPrevList(isReply ? replyList : rootList); + final ImmutableList.Builder builder = ImmutableList.builder(); + if (isReply) { + // in a reply list the first comment is the parent comment + builder.add(list.get(0)) + .add(comment) + .addAll(list.subList(1, list.size())); + } else { + builder.add(comment) + .addAll(list); + } + final MutableLiveData>> liveData = isReply ? replyList : rootList; + liveData.postValue(Resource.success(builder.build())); + } + + public void translate(@NonNull final Comment comment, + @NonNull final ServiceCallback callback) { + mediaService.translate(comment.getId(), "2", callback); + } + + public LiveData> deleteComment(@NonNull final Comment comment, final boolean isReply) { + final MutableLiveData> data = new MutableLiveData<>(Resource.loading(null)); + mediaService.deleteComment(postId, comment.getId(), new ServiceCallback() { + @Override + public void onSuccess(final Boolean result) { + if (result == null || !result) { + data.postValue(Resource.error(R.string.downloader_unknown_error, null)); + return; + } + removeComment(comment, isReply); + data.postValue(Resource.success(new Object())); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error deleting comment", t); + data.postValue(Resource.error(t.getMessage(), null)); + } + }); + return data; + } + + private void removeComment(@NonNull final Comment comment, final boolean isReply) { + final List list = getPrevList(isReply ? replyList : rootList); + final List updated = list.stream() + .filter(Objects::nonNull) + .filter(c -> !Objects.equals(c.getId(), comment.getId())) + .collect(Collectors.toList()); + final MutableLiveData>> liveData = isReply ? replyList : rootList; + liveData.postValue(Resource.success(updated)); + } + + public void clearReplies() { + prevRepliesCursor = repliesCursor; + prevRepliesHasNext = repliesHasNext; + repliesCursor = null; + repliesHasNext = true; + // cache prev reply list to save time and data if user clicks same comment again + prevReplies = getPrevList(replyList); + replyList.postValue(Resource.success(Collections.emptyList())); + } +} diff --git a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java index 6246cd67..eef7af9a 100644 --- a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java +++ b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java @@ -4,6 +4,8 @@ import android.util.Log; import androidx.annotation.NonNull; +import com.google.common.collect.ImmutableMap; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -290,6 +292,20 @@ public class GraphQLService extends BaseService { }); } + public Call fetchComments(final String shortCodeOrCommentId, + final boolean root, + final String cursor) { + final Map queryMap = new HashMap<>(); + queryMap.put("query_hash", root ? "bc3296d1ce80a24b1b6e40b1e72903f5" : "51fdd02b67508306ad4484ff574a0b62"); + final Map variables = ImmutableMap.of( + root ? "shortcode" : "comment_id", shortCodeOrCommentId, + "first", 50, + "after", cursor == null ? "" : cursor + ); + queryMap.put("variables", new JSONObject(variables).toString()); + return repository.fetch(queryMap); + } + public void fetchUser(final String username, final ServiceCallback callback) { final Call request = repository.getUser(username); @@ -305,7 +321,7 @@ public class GraphQLService extends BaseService { try { final JSONObject body = new JSONObject(rawBody); final JSONObject userJson = body.getJSONObject("graphql") - .getJSONObject(Constants.EXTRAS_USER); + .getJSONObject(Constants.EXTRAS_USER); boolean isPrivate = userJson.getBoolean("is_private"); final long id = userJson.optLong(Constants.EXTRAS_ID, 0); diff --git a/app/src/main/java/awais/instagrabber/webservices/MediaService.java b/app/src/main/java/awais/instagrabber/webservices/MediaService.java index d820b954..8eac4b35 100644 --- a/app/src/main/java/awais/instagrabber/webservices/MediaService.java +++ b/app/src/main/java/awais/instagrabber/webservices/MediaService.java @@ -1,12 +1,12 @@ package awais.instagrabber.webservices; -import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; import org.json.JSONException; import org.json.JSONObject; @@ -18,6 +18,7 @@ import java.util.Map; import java.util.Objects; import java.util.UUID; +import awais.instagrabber.models.Comment; import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.repositories.MediaRepository; import awais.instagrabber.repositories.requests.UploadFinishOptions; @@ -27,6 +28,7 @@ import awais.instagrabber.repositories.responses.MediaInfoResponse; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.utils.DateUtils; import awais.instagrabber.utils.MediaUploadHelper; +import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import retrofit2.Call; import retrofit2.Callback; @@ -170,7 +172,7 @@ public class MediaService extends BaseService { public void comment(@NonNull final String mediaId, @NonNull final String comment, final String replyToCommentId, - @NonNull final ServiceCallback callback) { + @NonNull final ServiceCallback callback) { final String module = "self_comments_v2"; final Map form = new HashMap<>(); // form.put("user_breadcrumb", userBreadcrumb(comment.length())); @@ -191,15 +193,33 @@ public class MediaService extends BaseService { final String body = response.body(); if (body == null) { Log.e(TAG, "Error occurred while creating comment"); - callback.onSuccess(false); + callback.onSuccess(null); return; } try { final JSONObject jsonObject = new JSONObject(body); - final String status = jsonObject.optString("status"); - callback.onSuccess(status.equals("ok")); - } catch (JSONException e) { - // Log.e(TAG, "Error parsing body", e); + // final String status = jsonObject.optString("status"); + final JSONObject commentJsonObject = jsonObject.optJSONObject("comment"); + Comment comment = null; + if (commentJsonObject != null) { + final JSONObject userJsonObject = commentJsonObject.optJSONObject("user"); + if (userJsonObject != null) { + final Gson gson = new Gson(); + final User user = gson.fromJson(userJsonObject.toString(), User.class); + comment = new Comment( + commentJsonObject.optString("pk"), + commentJsonObject.optString("text"), + commentJsonObject.optLong("created_at"), + 0, + false, + user, + 0, + !TextUtils.isEmpty(replyToCommentId) + ); + } + } + callback.onSuccess(comment); + } catch (Exception e) { callback.onFailure(e); } } @@ -221,7 +241,7 @@ public class MediaService extends BaseService { final List commentIds, @NonNull final ServiceCallback callback) { final Map form = new HashMap<>(); - form.put("comment_ids_to_delete", TextUtils.join(",", commentIds)); + form.put("comment_ids_to_delete", android.text.TextUtils.join(",", commentIds)); form.put("_csrftoken", csrfToken); form.put("_uid", userId); form.put("_uuid", deviceUuid); diff --git a/app/src/main/java/awais/instagrabber/webservices/interceptors/LoggingInterceptor.java b/app/src/main/java/awais/instagrabber/webservices/interceptors/LoggingInterceptor.java index e7bedd3b..02d02ba4 100644 --- a/app/src/main/java/awais/instagrabber/webservices/interceptors/LoggingInterceptor.java +++ b/app/src/main/java/awais/instagrabber/webservices/interceptors/LoggingInterceptor.java @@ -12,7 +12,7 @@ import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -class LoggingInterceptor implements Interceptor { +public class LoggingInterceptor implements Interceptor { private static final String TAG = "LoggingInterceptor"; @NonNull @@ -30,7 +30,11 @@ class LoggingInterceptor implements Interceptor { String content = ""; if (body != null) { contentType = body.contentType(); - content = body.string(); + try { + content = body.string(); + } catch (Exception e) { + Log.e(TAG, "intercept: ", e); + } Log.d(TAG, content); } final ResponseBody wrappedBody = ResponseBody.create(contentType, content); diff --git a/app/src/main/res/drawable/ic_outline_comments_24.xml b/app/src/main/res/drawable/ic_outline_comments_24.xml index fce13024..376f8df1 100644 --- a/app/src/main/res/drawable/ic_outline_comments_24.xml +++ b/app/src/main/res/drawable/ic_outline_comments_24.xml @@ -1,10 +1,10 @@ - - + android:viewportHeight="24"> + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_round_arrow_back_24.xml b/app/src/main/res/drawable/ic_round_arrow_back_24.xml new file mode 100644 index 00000000..26d33e0c --- /dev/null +++ b/app/src/main/res/drawable/ic_round_arrow_back_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_round_mode_comment_24.xml b/app/src/main/res/drawable/ic_round_mode_comment_24.xml index 366bca72..81bbaf21 100644 --- a/app/src/main/res/drawable/ic_round_mode_comment_24.xml +++ b/app/src/main/res/drawable/ic_round_mode_comment_24.xml @@ -1,10 +1,10 @@ - - + android:viewportHeight="24"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_comments.xml b/app/src/main/res/layout/fragment_comments.xml index 5ac646d6..7485a223 100644 --- a/app/src/main/res/layout/fragment_comments.xml +++ b/app/src/main/res/layout/fragment_comments.xml @@ -1,49 +1,74 @@ - + android:background="?colorSurface"> + + + + + app:layout_constraintBottom_toTopOf="@id/comment_field" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar"> + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_likes.xml b/app/src/main/res/layout/fragment_likes.xml index 15d2763d..4ae110ab 100644 --- a/app/src/main/res/layout/fragment_likes.xml +++ b/app/src/main/res/layout/fragment_likes.xml @@ -1,6 +1,5 @@ + android:clipToPadding="false" + tools:listitem="@layout/item_follow" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_comment.xml b/app/src/main/res/layout/item_comment.xml index 6e222821..d17f9c82 100755 --- a/app/src/main/res/layout/item_comment.xml +++ b/app/src/main/res/layout/item_comment.xml @@ -5,125 +5,135 @@ android:id="@+id/container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?android:selectableItemBackground" android:clickable="true" + android:descendantFocusability="afterDescendants" android:focusable="true" - android:orientation="horizontal" - android:padding="8dp"> + android:foreground="?selectableItemBackground" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="4dp"> - - - - - + android:orientation="vertical" + app:layout_constraintGuide_begin="56dp" /> - - - + android:layout_marginEnd="16dp" + app:actualImageScaleType="centerCrop" + app:layout_constraintEnd_toStartOf="@id/profile_pic_guideline" + app:layout_constraintHorizontal_bias="1" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintTop_toTopOf="parent" + app:size="small" /> - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + app:layout_constraintStart_toEndOf="@id/likes" + app:layout_constraintTop_toBottomOf="@id/comment_barrier" + tools:text="9999" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/item_comment_small.xml b/app/src/main/res/layout/item_comment_small.xml deleted file mode 100755 index 544c93e9..00000000 --- a/app/src/main/res/layout/item_comment_small.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_follow.xml b/app/src/main/res/layout/item_follow.xml index ab2d0bd9..4683be31 100755 --- a/app/src/main/res/layout/item_follow.xml +++ b/app/src/main/res/layout/item_follow.xml @@ -1,51 +1,42 @@ - + android:background="?android:selectableItemBackground" + android:padding="16dp"> - + - + - - - - - - - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/menu/comment_options_menu.xml b/app/src/main/res/menu/comment_options_menu.xml new file mode 100644 index 00000000..cd453454 --- /dev/null +++ b/app/src/main/res/menu/comment_options_menu.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/comments_nav_graph.xml b/app/src/main/res/navigation/comments_nav_graph.xml index 82ffd80c..0cec9091 100644 --- a/app/src/main/res/navigation/comments_nav_graph.xml +++ b/app/src/main/res/navigation/comments_nav_graph.xml @@ -29,7 +29,7 @@ + @@ -22,6 +23,7 @@ + @@ -37,4 +39,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/color.xml b/app/src/main/res/values/color.xml index fd773071..a4ea3e6f 100755 --- a/app/src/main/res/values/color.xml +++ b/app/src/main/res/values/color.xml @@ -25,8 +25,9 @@ #efefef - #888888 - #40FF69B4 + #FF082654 + @color/grey_300 + #FFFFFF #80FFFFFF @@ -63,6 +64,10 @@ #2979FF #2962FF + #01579b + + #1A237E + #EFEBE9 #D7CCC8 #BCAAA4 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index fe527b2d..2e12b35d 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -9,6 +9,7 @@ 40dp 24dp + 32dp 40dp 48dp 90dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b3f95e47..9c6856e6 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -214,7 +214,7 @@ Like comment Unlike comment Translate comment - Delete comment + No empty comments! Do you want to search the username? Do you want to search the hashtag? @@ -381,6 +381,10 @@ %d like %d likes + + %d reply + %d replies + %d comment %d comments diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index d013e888..3c212ab7 100755 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -261,13 +261,13 @@ @color/white - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index d781845f..660a2189 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -53,6 +53,7 @@ @color/blue_800 @color/black @style/Widget.MaterialComponents.TabLayout.Light.White + @color/parent_comment_light_white @@ -146,7 +149,8 @@ @color/deep_purple_400 @color/deep_purple_600 @style/Widget.MaterialComponents.TabLayout.Dark.Black - @style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dark.Black + @style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dark.Black + @color/parent_comment_dark_materialdark