Browse Source
Improve comments viewer ui and ux. Update ui for likes viewer and follows viewer.
renovate/org.robolectric-robolectric-4.x
Improve comments viewer ui and ux. Update ui for likes viewer and follows viewer.
renovate/org.robolectric-robolectric-4.x
45 changed files with 2132 additions and 1539 deletions
-
195app/src/main/java/awais/instagrabber/adapters/CommentsAdapter.java
-
2app/src/main/java/awais/instagrabber/adapters/LikesAdapter.java
-
209app/src/main/java/awais/instagrabber/adapters/viewholder/CommentViewHolder.java
-
21app/src/main/java/awais/instagrabber/adapters/viewholder/FollowsViewHolder.java
-
95app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ChildCommentViewHolder.java
-
95app/src/main/java/awais/instagrabber/adapters/viewholder/comments/ParentCommentViewHolder.java
-
268app/src/main/java/awais/instagrabber/asyncs/CommentsFetcher.java
-
6app/src/main/java/awais/instagrabber/customviews/ProfilePicView.java
-
24app/src/main/java/awais/instagrabber/customviews/RamboTextViewV2.java
-
95app/src/main/java/awais/instagrabber/customviews/TextViewDrawableSize.java
-
77app/src/main/java/awais/instagrabber/customviews/UsernameTextView.java
-
478app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java
-
11app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java
-
237app/src/main/java/awais/instagrabber/fragments/comments/CommentsViewerFragment.java
-
288app/src/main/java/awais/instagrabber/fragments/comments/Helper.java
-
214app/src/main/java/awais/instagrabber/fragments/comments/RepliesFragment.java
-
4app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java
-
4app/src/main/java/awais/instagrabber/fragments/main/FeedFragment.java
-
3app/src/main/java/awais/instagrabber/fragments/main/ProfileFragment.java
-
117app/src/main/java/awais/instagrabber/models/Comment.java
-
103app/src/main/java/awais/instagrabber/models/CommentModel.java
-
51app/src/main/java/awais/instagrabber/repositories/responses/GraphQLCommentsFetchResponse.java
-
12app/src/main/java/awais/instagrabber/utils/Utils.java
-
2app/src/main/java/awais/instagrabber/viewmodels/AppStateViewModel.java
-
19app/src/main/java/awais/instagrabber/viewmodels/CommentsViewModel.java
-
447app/src/main/java/awais/instagrabber/viewmodels/CommentsViewerViewModel.java
-
16app/src/main/java/awais/instagrabber/webservices/GraphQLService.java
-
36app/src/main/java/awais/instagrabber/webservices/MediaService.java
-
6app/src/main/java/awais/instagrabber/webservices/interceptors/LoggingInterceptor.java
-
8app/src/main/res/drawable/ic_outline_comments_24.xml
-
10app/src/main/res/drawable/ic_round_arrow_back_24.xml
-
8app/src/main/res/drawable/ic_round_mode_comment_24.xml
-
51app/src/main/res/layout/fragment_comments.xml
-
4app/src/main/res/layout/fragment_likes.xml
-
192app/src/main/res/layout/item_comment.xml
-
109app/src/main/res/layout/item_comment_small.xml
-
77app/src/main/res/layout/item_follow.xml
-
9app/src/main/res/menu/comment_options_menu.xml
-
2app/src/main/res/navigation/comments_nav_graph.xml
-
7app/src/main/res/values/attrs.xml
-
9app/src/main/res/values/color.xml
-
1app/src/main/res/values/dimens.xml
-
6app/src/main/res/values/strings.xml
-
6app/src/main/res/values/styles.xml
-
7app/src/main/res/values/themes.xml
@ -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); |
||||
|
// }); |
||||
|
// } |
||||
|
} |
@ -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)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -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)); |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -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<Void, Void, List<CommentModel>> { |
|
||||
private static final String TAG = "CommentsFetcher"; |
|
||||
|
|
||||
private final String shortCode, endCursor; |
|
||||
private final FetchListener<List<CommentModel>> fetchListener; |
|
||||
|
|
||||
public CommentsFetcher(final String shortCode, final String endCursor, final FetchListener<List<CommentModel>> fetchListener) { |
|
||||
this.shortCode = shortCode; |
|
||||
this.endCursor = endCursor; |
|
||||
this.fetchListener = fetchListener; |
|
||||
} |
|
||||
|
|
||||
@NonNull |
|
||||
@Override |
|
||||
protected List<CommentModel> 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<CommentModel> commentModels = getParentComments(); |
|
||||
if (commentModels != null) { |
|
||||
for (final CommentModel commentModel : commentModels) { |
|
||||
final List<CommentModel> 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<CommentModel> 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<CommentModel> result) { |
|
||||
if (fetchListener != null) fetchListener.onResult(result); |
|
||||
} |
|
||||
|
|
||||
@NonNull |
|
||||
private synchronized List<CommentModel> getChildComments(final String commentId) { |
|
||||
final List<CommentModel> 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<CommentModel> getParentComments() { |
|
||||
final List<CommentModel> 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<CommentModel> 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; |
|
||||
} |
|
||||
} |
|
@ -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(); |
||||
|
} |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
@ -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<Void, Void, List<CommentModel>> currentlyRunning; |
|
||||
private CommentsViewModel commentsViewModel; |
|
||||
|
|
||||
private final FetchListener<List<CommentModel>> fetchListener = new FetchListener<List<CommentModel>>() { |
|
||||
@Override |
|
||||
public void doBefore() { |
|
||||
binding.swipeRefreshLayout.setRefreshing(true); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onResult(final List<CommentModel> commentModels) { |
|
||||
if (commentModels != null && commentModels.size() > 0) { |
|
||||
endCursor = commentModels.get(0).getEndCursor(); |
|
||||
hasNextPage = commentModels.get(0).hasNextPage(); |
|
||||
List<CommentModel> 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<Boolean>() { |
|
||||
@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<Boolean>() { |
|
||||
@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<Boolean>() { |
|
||||
@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<String>() { |
|
||||
@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<Boolean>() { |
|
||||
@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; |
|
||||
} |
|
||||
} |
|
@ -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<View> 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<Resource<Object>> resourceLiveData = viewModel.comment(text, false); |
||||
|
resourceLiveData.observe(getViewLifecycleOwner(), new Observer<Resource<Object>>() { |
||||
|
@Override |
||||
|
public void onChanged(final Resource<Object> 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<List<Comment>> 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; |
||||
|
} |
||||
|
} |
@ -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<Comment, Boolean, Void> 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<Resource<Object>> resourceLiveData = viewModel.likeComment(comment, liked, isReply); |
||||
|
resourceLiveData.observe(lifecycleOwner, new Observer<Resource<Object>>() { |
||||
|
@Override |
||||
|
public void onChanged(final Resource<Object> 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<String>() { |
||||
|
@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<Resource<Object>> resourceLiveData = viewModel.deleteComment(comment, isReply); |
||||
|
resourceLiveData.observe(lifecycleOwner, new Observer<Resource<Object>>() { |
||||
|
@Override |
||||
|
public void onChanged(final Resource<Object> 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<String, Void> 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<Resource<Object>> resourceLiveData, |
||||
|
@NonNull final Observer<Resource<Object>> 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); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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<Resource<Object>> resourceLiveData = viewModel.comment(text, true); |
||||
|
resourceLiveData.observe(getViewLifecycleOwner(), new Observer<Resource<Object>>() { |
||||
|
@Override |
||||
|
public void onChanged(final Resource<Object> 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<List<Comment>> 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; |
||||
|
} |
||||
|
} |
@ -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 + |
||||
|
'}'; |
||||
|
} |
||||
|
} |
@ -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<CommentModel> 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<CommentModel> getChildCommentModels() { |
|
||||
return childCommentModels; |
|
||||
} |
|
||||
|
|
||||
public void setChildCommentModels(final List<CommentModel> 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 : "\"") + '}'; |
|
||||
// } |
|
||||
// } |
|
||||
} |
|
@ -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<Comment> comments; |
||||
|
|
||||
|
public GraphQLCommentsFetchResponse(final int count, |
||||
|
final String cursor, |
||||
|
final boolean hasNext, |
||||
|
final List<Comment> 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<Comment> getComments() { |
||||
|
return comments; |
||||
|
} |
||||
|
|
||||
|
@NonNull |
||||
|
@Override |
||||
|
public String toString() { |
||||
|
return "GraphQLCommentsFetchResponse{" + |
||||
|
"count=" + count + |
||||
|
", cursor='" + cursor + '\'' + |
||||
|
", hasNext=" + hasNext + |
||||
|
", comments=" + comments + |
||||
|
'}'; |
||||
|
} |
||||
|
} |
@ -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<CommentModel>> list; |
|
||||
|
|
||||
public MutableLiveData<List<CommentModel>> getList() { |
|
||||
if (list == null) { |
|
||||
list = new MutableLiveData<>(); |
|
||||
} |
|
||||
return list; |
|
||||
} |
|
||||
} |
|
@ -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<Boolean> isLoggedIn = new MutableLiveData<>(false); |
||||
|
private final MutableLiveData<Long> currentUserId = new MutableLiveData<>(0L); |
||||
|
private final MutableLiveData<Resource<List<Comment>>> rootList = new MutableLiveData<>(); |
||||
|
private final MutableLiveData<Integer> rootCount = new MutableLiveData<>(0); |
||||
|
private final MutableLiveData<Resource<List<Comment>>> 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<Comment> 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<Boolean> isLoggedIn() { |
||||
|
return isLoggedIn; |
||||
|
} |
||||
|
|
||||
|
public LiveData<Long> getCurrentUserId() { |
||||
|
return currentUserId; |
||||
|
} |
||||
|
|
||||
|
@Nullable |
||||
|
public Comment getRepliesParent() { |
||||
|
return repliesParent; |
||||
|
} |
||||
|
|
||||
|
public LiveData<Resource<List<Comment>>> getRootList() { |
||||
|
return rootList; |
||||
|
} |
||||
|
|
||||
|
public LiveData<Resource<List<Comment>>> getReplyList() { |
||||
|
return replyList; |
||||
|
} |
||||
|
|
||||
|
public LiveData<Integer> 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<Comment> 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<String> request = service.fetchComments(shortCodeOrCommentId, root, root ? rootCursor : repliesCursor); |
||||
|
enqueueRequest(request, root, shortCodeOrCommentId, new ServiceCallback<GraphQLCommentsFetchResponse>() { |
||||
|
@Override |
||||
|
public void onSuccess(final GraphQLCommentsFetchResponse result) { |
||||
|
// Log.d(TAG, "onSuccess: " + result); |
||||
|
List<Comment> 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.<Comment>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<String> request, |
||||
|
final boolean root, |
||||
|
final String shortCodeOrCommentId, |
||||
|
final ServiceCallback<GraphQLCommentsFetchResponse> callback) { |
||||
|
request.enqueue(new Callback<String>() { |
||||
|
@Override |
||||
|
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> 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<Comment> 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<String> 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<Comment> getPrevList(@NonNull final LiveData<Resource<List<Comment>>> list) { |
||||
|
if (list.getValue() == null) return Collections.emptyList(); |
||||
|
final Resource<List<Comment>> listResource = list.getValue(); |
||||
|
if (listResource.data == null) return Collections.emptyList(); |
||||
|
return listResource.data; |
||||
|
} |
||||
|
|
||||
|
private List<Comment> mergeList(@NonNull final LiveData<Resource<List<Comment>>> list, |
||||
|
final List<Comment> comments) { |
||||
|
final List<Comment> prevList = getPrevList(list); |
||||
|
if (comments == null) { |
||||
|
return prevList; |
||||
|
} |
||||
|
return ImmutableList.<Comment>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<Resource<Object>> likeComment(@NonNull final Comment comment, final boolean liked, final boolean isReply) { |
||||
|
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(Resource.loading(null)); |
||||
|
final ServiceCallback<Boolean> callback = new ServiceCallback<Boolean>() { |
||||
|
@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<Comment> list = getPrevList(isReply ? replyList : rootList); |
||||
|
if (list == null) return; |
||||
|
final List<Comment> 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<Resource<List<Comment>>> liveData = isReply ? replyList : rootList; |
||||
|
liveData.postValue(Resource.success(copy)); |
||||
|
} catch (Exception e) { |
||||
|
Log.e(TAG, "setLiked: ", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public LiveData<Resource<Object>> comment(@NonNull final String text, |
||||
|
final boolean isReply) { |
||||
|
final MutableLiveData<Resource<Object>> 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<Comment>() { |
||||
|
@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<Comment> list = getPrevList(isReply ? replyList : rootList); |
||||
|
final ImmutableList.Builder<Comment> 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<Resource<List<Comment>>> liveData = isReply ? replyList : rootList; |
||||
|
liveData.postValue(Resource.success(builder.build())); |
||||
|
} |
||||
|
|
||||
|
public void translate(@NonNull final Comment comment, |
||||
|
@NonNull final ServiceCallback<String> callback) { |
||||
|
mediaService.translate(comment.getId(), "2", callback); |
||||
|
} |
||||
|
|
||||
|
public LiveData<Resource<Object>> deleteComment(@NonNull final Comment comment, final boolean isReply) { |
||||
|
final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(Resource.loading(null)); |
||||
|
mediaService.deleteComment(postId, comment.getId(), new ServiceCallback<Boolean>() { |
||||
|
@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<Comment> list = getPrevList(isReply ? replyList : rootList); |
||||
|
final List<Comment> updated = list.stream() |
||||
|
.filter(Objects::nonNull) |
||||
|
.filter(c -> !Objects.equals(c.getId(), comment.getId())) |
||||
|
.collect(Collectors.toList()); |
||||
|
final MutableLiveData<Resource<List<Comment>>> 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())); |
||||
|
} |
||||
|
} |
@ -1,10 +1,10 @@ |
|||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:width="24dp" |
android:width="24dp" |
||||
android:height="24dp" |
android:height="24dp" |
||||
|
android:tint="?attr/colorControlNormal" |
||||
android:viewportWidth="24" |
android:viewportWidth="24" |
||||
android:viewportHeight="24" |
|
||||
android:tint="?attr/colorControlNormal"> |
|
||||
|
android:viewportHeight="24"> |
||||
<path |
<path |
||||
android:fillColor="@android:color/white" |
|
||||
android:pathData="M20,17.17L18.83,16H4V4h16v13.17zM20,2H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h14l4,4V4c0,-1.1 -0.9,-2 -2,-2z"/> |
|
||||
|
android:fillColor="#000" |
||||
|
android:pathData="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10Z" /> |
||||
</vector> |
</vector> |
@ -0,0 +1,10 @@ |
|||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
android:width="24dp" |
||||
|
android:height="24dp" |
||||
|
android:viewportWidth="24" |
||||
|
android:viewportHeight="24" |
||||
|
android:tint="?attr/colorControlNormal"> |
||||
|
<path |
||||
|
android:fillColor="@android:color/white" |
||||
|
android:pathData="M19,11H7.83l4.88,-4.88c0.39,-0.39 0.39,-1.03 0,-1.42 -0.39,-0.39 -1.02,-0.39 -1.41,0l-6.59,6.59c-0.39,0.39 -0.39,1.02 0,1.41l6.59,6.59c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L7.83,13H19c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1z"/> |
||||
|
</vector> |
@ -1,10 +1,10 @@ |
|||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:width="24dp" |
android:width="24dp" |
||||
android:height="24dp" |
android:height="24dp" |
||||
|
android:tint="?attr/colorControlNormal" |
||||
android:viewportWidth="24" |
android:viewportWidth="24" |
||||
android:viewportHeight="24" |
|
||||
android:tint="?attr/colorControlNormal"> |
|
||||
|
android:viewportHeight="24"> |
||||
<path |
<path |
||||
android:fillColor="@android:color/white" |
|
||||
android:pathData="M22,4c0,-1.1 -0.9,-2 -2,-2H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h14l4,4V4z"/> |
|
||||
|
android:fillColor="#000" |
||||
|
android:pathData="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9Z" /> |
||||
</vector> |
</vector> |
@ -1,49 +1,74 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto" |
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
xmlns:tools="http://schemas.android.com/tools" |
xmlns:tools="http://schemas.android.com/tools" |
||||
android:layout_width="match_parent" |
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
android:layout_height="match_parent" |
||||
android:orientation="vertical"> |
|
||||
|
android:background="?colorSurface"> |
||||
|
|
||||
|
<androidx.fragment.app.FragmentContainerView |
||||
|
android:id="@+id/replies_container_view" |
||||
|
android:layout_width="0dp" |
||||
|
android:layout_height="0dp" |
||||
|
android:elevation="10dp" |
||||
|
app:layout_constraintBottom_toBottomOf="parent" |
||||
|
app:layout_constraintEnd_toEndOf="parent" |
||||
|
app:layout_constraintStart_toStartOf="parent" |
||||
|
app:layout_constraintTop_toTopOf="parent" /> |
||||
|
|
||||
|
<com.google.android.material.appbar.MaterialToolbar |
||||
|
android:id="@+id/toolbar" |
||||
|
android:layout_width="0dp" |
||||
|
android:layout_height="wrap_content" |
||||
|
app:layout_constraintBottom_toTopOf="@id/swipe_refresh_layout" |
||||
|
app:layout_constraintEnd_toEndOf="parent" |
||||
|
app:layout_constraintStart_toStartOf="parent" |
||||
|
app:layout_constraintTop_toTopOf="parent" /> |
||||
|
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout |
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout |
||||
android:id="@+id/swipeRefreshLayout" |
|
||||
android:layout_width="match_parent" |
|
||||
|
android:id="@+id/swipe_refresh_layout" |
||||
|
android:layout_width="0dp" |
||||
android:layout_height="0dp" |
android:layout_height="0dp" |
||||
android:layout_weight="1"> |
|
||||
|
app:layout_constraintBottom_toTopOf="@id/comment_field" |
||||
|
app:layout_constraintEnd_toEndOf="parent" |
||||
|
app:layout_constraintStart_toStartOf="parent" |
||||
|
app:layout_constraintTop_toBottomOf="@id/toolbar"> |
||||
|
|
||||
<androidx.recyclerview.widget.RecyclerView |
<androidx.recyclerview.widget.RecyclerView |
||||
android:id="@+id/rvComments" |
|
||||
|
android:id="@+id/comments" |
||||
android:layout_width="match_parent" |
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
android:layout_height="match_parent" |
||||
android:clipToPadding="false" |
android:clipToPadding="false" |
||||
tools:listitem="@layout/item_comment" /> |
tools:listitem="@layout/item_comment" /> |
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> |
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> |
||||
|
|
||||
|
<!--app:startIconDrawable="@drawable/ic_close_24"--> |
||||
<com.google.android.material.textfield.TextInputLayout |
<com.google.android.material.textfield.TextInputLayout |
||||
android:id="@+id/commentField" |
|
||||
android:layout_width="match_parent" |
|
||||
|
android:id="@+id/comment_field" |
||||
|
android:layout_width="0dp" |
||||
android:layout_height="wrap_content" |
android:layout_height="wrap_content" |
||||
android:layout_margin="4dp" |
android:layout_margin="4dp" |
||||
android:hint="@string/comment_hint" |
android:hint="@string/comment_hint" |
||||
android:visibility="gone" |
android:visibility="gone" |
||||
app:counterEnabled="true" |
|
||||
|
app:counterEnabled="false" |
||||
app:counterMaxLength="2200" |
app:counterMaxLength="2200" |
||||
app:endIconDrawable="@drawable/ic_round_send_24" |
app:endIconDrawable="@drawable/ic_round_send_24" |
||||
app:endIconMode="custom" |
app:endIconMode="custom" |
||||
app:startIconDrawable="@drawable/ic_close_24" |
|
||||
|
app:layout_constraintBottom_toBottomOf="parent" |
||||
|
app:layout_constraintEnd_toEndOf="parent" |
||||
|
app:layout_constraintStart_toStartOf="parent" |
||||
|
app:layout_constraintTop_toBottomOf="@id/swipe_refresh_layout" |
||||
tools:visibility="visible"> |
tools:visibility="visible"> |
||||
|
|
||||
<com.google.android.material.textfield.TextInputEditText |
<com.google.android.material.textfield.TextInputEditText |
||||
android:id="@+id/commentText" |
|
||||
|
android:id="@+id/comment_text" |
||||
android:layout_width="match_parent" |
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
android:layout_height="wrap_content" |
||||
android:autofillHints="no" |
android:autofillHints="no" |
||||
android:inputType="textMultiLine" |
android:inputType="textMultiLine" |
||||
android:maxLength="2200" |
android:maxLength="2200" |
||||
android:maxLines="10" |
|
||||
android:scrollHorizontally="false" |
android:scrollHorizontally="false" |
||||
tools:text="test" /> |
tools:text="test" /> |
||||
|
|
||||
</com.google.android.material.textfield.TextInputLayout> |
</com.google.android.material.textfield.TextInputLayout> |
||||
</androidx.appcompat.widget.LinearLayoutCompat> |
|
||||
|
</androidx.constraintlayout.widget.ConstraintLayout> |
@ -1,109 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto" |
|
||||
xmlns:tools="http://schemas.android.com/tools" |
|
||||
android:id="@+id/container" |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="wrap_content" |
|
||||
android:paddingStart="40dp" |
|
||||
android:paddingTop="8dp" |
|
||||
android:paddingEnd="8dp" |
|
||||
android:paddingBottom="2dp"> |
|
||||
|
|
||||
<com.facebook.drawee.view.SimpleDraweeView |
|
||||
android:id="@+id/ivProfilePic" |
|
||||
android:layout_width="50dp" |
|
||||
android:layout_height="50dp" |
|
||||
app:actualImageScaleType="centerCrop" |
|
||||
app:layout_constraintEnd_toStartOf="@id/tvUsername" |
|
||||
app:layout_constraintStart_toStartOf="parent" |
|
||||
app:layout_constraintTop_toTopOf="parent" |
|
||||
app:roundAsCircle="true" |
|
||||
tools:background="@mipmap/ic_launcher" /> |
|
||||
|
|
||||
<androidx.appcompat.widget.AppCompatTextView |
|
||||
android:id="@+id/tvUsername" |
|
||||
android:layout_width="0dp" |
|
||||
android:layout_height="wrap_content" |
|
||||
android:layout_gravity="start" |
|
||||
android:paddingStart="4dp" |
|
||||
android:paddingTop="2dp" |
|
||||
android:paddingEnd="4dp" |
|
||||
android:paddingBottom="2dp" |
|
||||
android:singleLine="true" |
|
||||
android:textAppearance="@style/TextAppearance.AppCompat" |
|
||||
android:textColor="?android:textColorPrimary" |
|
||||
android:textStyle="bold" |
|
||||
app:layout_constraintBottom_toTopOf="@id/tvComment" |
|
||||
app:layout_constraintStart_toEndOf="@id/ivProfilePic" |
|
||||
app:layout_constraintTop_toTopOf="parent" |
|
||||
tools:text="username" /> |
|
||||
|
|
||||
<androidx.appcompat.widget.AppCompatImageView |
|
||||
android:id="@+id/isVerified" |
|
||||
android:layout_width="0dp" |
|
||||
android:layout_height="0dp" |
|
||||
android:scaleType="fitStart" |
|
||||
android:visibility="gone" |
|
||||
app:layout_constraintBottom_toBottomOf="@id/tvUsername" |
|
||||
app:layout_constraintEnd_toEndOf="parent" |
|
||||
app:layout_constraintStart_toEndOf="@id/tvUsername" |
|
||||
app:layout_constraintTop_toTopOf="@id/tvUsername" |
|
||||
app:srcCompat="@drawable/verified" |
|
||||
tools:visibility="visible" /> |
|
||||
|
|
||||
<awais.instagrabber.customviews.RamboTextViewV2 |
|
||||
android:id="@+id/tvComment" |
|
||||
android:layout_width="0dp" |
|
||||
android:layout_height="wrap_content" |
|
||||
android:layout_gravity="start" |
|
||||
android:autoLink="web|email" |
|
||||
android:clickable="true" |
|
||||
android:ellipsize="end" |
|
||||
android:focusable="true" |
|
||||
android:linksClickable="true" |
|
||||
android:paddingStart="4dp" |
|
||||
android:paddingTop="2dp" |
|
||||
android:paddingEnd="4dp" |
|
||||
android:paddingBottom="2dp" |
|
||||
android:textAppearance="@style/TextAppearance.AppCompat" |
|
||||
app:layout_constraintBottom_toTopOf="@id/tvLikes" |
|
||||
app:layout_constraintEnd_toEndOf="parent" |
|
||||
app:layout_constraintStart_toEndOf="@id/ivProfilePic" |
|
||||
app:layout_constraintTop_toBottomOf="@id/tvUsername" |
|
||||
tools:text="comment comment comment comment comment comment comment comment |
|
||||
comment comment comment comment comment comment comment comment comment comment comment comment comment comment |
|
||||
comment comment comment comment comment comment " /> |
|
||||
|
|
||||
<androidx.appcompat.widget.AppCompatTextView |
|
||||
android:id="@+id/tvLikes" |
|
||||
android:layout_width="wrap_content" |
|
||||
android:layout_height="0dp" |
|
||||
android:paddingStart="4dp" |
|
||||
android:paddingTop="2dp" |
|
||||
android:paddingEnd="4dp" |
|
||||
android:paddingBottom="2dp" |
|
||||
android:scrollbars="none" |
|
||||
android:singleLine="true" |
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Caption" |
|
||||
app:layout_constraintBottom_toBottomOf="parent" |
|
||||
app:layout_constraintEnd_toStartOf="@id/tvDate" |
|
||||
app:layout_constraintStart_toEndOf="@id/ivProfilePic" |
|
||||
app:layout_constraintTop_toBottomOf="@id/tvComment" |
|
||||
tools:text="likes" /> |
|
||||
|
|
||||
<androidx.appcompat.widget.AppCompatTextView |
|
||||
android:id="@+id/tvDate" |
|
||||
android:layout_width="0dp" |
|
||||
android:layout_height="wrap_content" |
|
||||
android:gravity="end" |
|
||||
android:singleLine="true" |
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Caption" |
|
||||
android:textStyle="italic" |
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/tvLikes" |
|
||||
app:layout_constraintBottom_toBottomOf="parent" |
|
||||
app:layout_constraintEnd_toEndOf="parent" |
|
||||
app:layout_constraintStart_toEndOf="@id/tvLikes" |
|
||||
app:layout_constraintTop_toBottomOf="@id/tvComment" |
|
||||
tools:text="date....." /> |
|
||||
</androidx.constraintlayout.widget.ConstraintLayout> |
|
@ -1,51 +1,42 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
<?xml version="1.0" encoding="utf-8"?> |
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
xmlns:app="http://schemas.android.com/apk/res-auto" |
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||
|
xmlns:tools="http://schemas.android.com/tools" |
||||
android:layout_width="match_parent" |
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
android:layout_height="wrap_content" |
||||
android:foreground="?android:selectableItemBackground" |
|
||||
android:padding="8dp"> |
|
||||
|
android:background="?android:selectableItemBackground" |
||||
|
android:padding="16dp"> |
||||
|
|
||||
<com.facebook.drawee.view.SimpleDraweeView |
|
||||
android:id="@+id/ivProfilePic" |
|
||||
android:layout_width="60dp" |
|
||||
android:layout_height="60dp" |
|
||||
android:adjustViewBounds="true" |
|
||||
android:scaleType="fitCenter" /> |
|
||||
|
|
||||
<LinearLayout |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="60dp" |
|
||||
android:layout_marginStart="66dp" |
|
||||
android:layout_marginEnd="26dp" |
|
||||
android:orientation="vertical"> |
|
||||
|
<awais.instagrabber.customviews.ProfilePicView |
||||
|
android:id="@+id/profile_pic" |
||||
|
android:layout_width="wrap_content" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_marginEnd="16dp" |
||||
|
app:layout_constraintBottom_toBottomOf="parent" |
||||
|
app:layout_constraintEnd_toStartOf="@id/username" |
||||
|
app:layout_constraintStart_toStartOf="parent" |
||||
|
app:layout_constraintTop_toTopOf="parent" |
||||
|
app:size="small" /> |
||||
|
|
||||
<androidx.appcompat.widget.AppCompatTextView |
|
||||
android:id="@+id/tvFullName" |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="0dp" |
|
||||
android:layout_weight="1" |
|
||||
android:gravity="center_vertical" |
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Large" |
|
||||
app:autoSizeMaxTextSize="28sp" |
|
||||
app:autoSizeMinTextSize="16sp" |
|
||||
app:autoSizeTextType="uniform" /> |
|
||||
|
<awais.instagrabber.customviews.UsernameTextView |
||||
|
android:id="@+id/username" |
||||
|
android:layout_width="0dp" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" |
||||
|
app:layout_constraintBottom_toTopOf="@id/full_name" |
||||
|
app:layout_constraintEnd_toEndOf="parent" |
||||
|
app:layout_constraintStart_toEndOf="@id/profile_pic" |
||||
|
app:layout_constraintTop_toTopOf="parent" |
||||
|
tools:text="username" /> |
||||
|
|
||||
<androidx.appcompat.widget.AppCompatTextView |
<androidx.appcompat.widget.AppCompatTextView |
||||
android:id="@+id/tvUsername" |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="0dp" |
|
||||
android:layout_weight="1" |
|
||||
android:gravity="center_vertical" |
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium" /> |
|
||||
</LinearLayout> |
|
||||
|
|
||||
<androidx.appcompat.widget.AppCompatImageView |
|
||||
android:id="@+id/isAdmin" |
|
||||
android:layout_width="20dp" |
|
||||
android:layout_height="60dp" |
|
||||
android:layout_gravity="end" |
|
||||
android:scaleType="fitCenter" |
|
||||
android:visibility="gone" |
|
||||
app:srcCompat="@drawable/ic_star_24" /> |
|
||||
</FrameLayout> |
|
||||
|
android:id="@+id/full_name" |
||||
|
android:layout_width="0dp" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2" |
||||
|
app:layout_constraintBottom_toBottomOf="parent" |
||||
|
app:layout_constraintEnd_toEndOf="parent" |
||||
|
app:layout_constraintStart_toStartOf="@id/username" |
||||
|
app:layout_constraintTop_toBottomOf="@id/username" |
||||
|
tools:text="Full Name" /> |
||||
|
</androidx.constraintlayout.widget.ConstraintLayout> |
@ -0,0 +1,9 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
|
<item |
||||
|
android:id="@+id/translate" |
||||
|
android:title="@string/comment_viewer_translate_comment" /> |
||||
|
<item |
||||
|
android:id="@+id/delete" |
||||
|
android:title="@string/delete" /> |
||||
|
</menu> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue