From 28af696e0123b6b8fa8f2061f698757a3c6a2b77 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sat, 26 Dec 2020 13:14:08 -0500 Subject: [PATCH] support for viewing comment likes --- .../fragments/CommentsViewerFragment.java | 35 ++++++-- .../fragments/LikesViewerFragment.java | 66 ++++++++++++++-- .../fragments/PostViewV2Fragment.java | 1 + .../repositories/MediaRepository.java | 5 +- .../GraphQLUserListFetchResponse.java | 79 +++++++++++++++++++ .../webservices/GraphQLService.java | 59 ++++++++++++++ .../webservices/MediaService.java | 3 +- .../res/navigation/comments_nav_graph.xml | 15 ++++ .../navigation/direct_messages_nav_graph.xml | 12 ++- .../res/navigation/discover_nav_graph.xml | 12 ++- .../main/res/navigation/feed_nav_graph.xml | 4 + .../main/res/navigation/hashtag_nav_graph.xml | 4 + .../main/res/navigation/likes_nav_graph.xml | 8 ++ .../res/navigation/location_nav_graph.xml | 4 + .../notification_viewer_nav_graph.xml | 16 +++- .../main/res/navigation/profile_nav_graph.xml | 5 +- app/src/main/res/values/strings.xml | 1 + 17 files changed, 304 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/awais/instagrabber/repositories/responses/GraphQLUserListFetchResponse.java diff --git a/app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java index 15d7092a..85b6e00a 100644 --- a/app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/CommentsViewerFragment.java @@ -25,6 +25,7 @@ import androidx.appcompat.widget.LinearLayoutCompat; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.ViewModelProvider; +import androidx.navigation.NavController; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.LinearLayoutManager; @@ -288,6 +289,7 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl 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), @@ -298,6 +300,7 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl 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), @@ -306,7 +309,8 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl } else { commentDialogList = new String[]{ resources.getString(R.string.open_profile), - resources.getString(R.string.comment_viewer_copy_comment) + resources.getString(R.string.comment_viewer_copy_comment), + resources.getString(R.string.comment_viewer_see_likers) }; } final Context context = getContext(); @@ -321,7 +325,17 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl case 1: // copy comment Utils.copyText(context, "@" + profileModel.getUsername() + ": " + commentModel.getText()); break; - case 2: // reply to comment + 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); @@ -333,7 +347,7 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl imm.showSoftInput(binding.commentText, 0); }, 200); break; - case 3: // like/unlike comment + case 4: // like/unlike comment if (csrfToken == null) { return; } @@ -373,7 +387,7 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl } }); break; - case 4: // translate comment + case 5: // translate comment mediaService.translate(commentModel.getId(), "2", new ServiceCallback() { @Override public void onSuccess(final String result) { @@ -395,7 +409,7 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl } }); break; - case 5: // delete comment + case 6: // delete comment final String userId = CookieUtils.getUserIdFromCookie(cookie); if (userId == null) return; mediaService.deleteComment( @@ -440,4 +454,15 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl } } } + + @Nullable + private NavController getNavController() { + NavController navController = null; + try { + navController = NavHostFragment.findNavController(this); + } catch (IllegalStateException e) { + Log.e(TAG, "navigateToProfile", e); + } + return navController; + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java b/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java index 40174383..ddb93fb0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/LikesViewerFragment.java @@ -14,28 +14,30 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.LinearLayoutCompat; -import androidx.lifecycle.ViewModelProvider; -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.List; -import awais.instagrabber.BuildConfig; import awais.instagrabber.R; import awais.instagrabber.adapters.LikesAdapter; +import awais.instagrabber.customviews.helpers.RecyclerLazyLoader; import awais.instagrabber.databinding.FragmentLikesBinding; import awais.instagrabber.models.ProfileModel; +import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse; import awais.instagrabber.utils.Constants; +import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; +import awais.instagrabber.webservices.GraphQLService; import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.ServiceCallback; +import static awais.instagrabber.utils.Utils.settingsHelper; + public final class LikesViewerFragment extends BottomSheetDialogFragment implements SwipeRefreshLayout.OnRefreshListener { private static final String TAG = "LikesViewerFragment"; @@ -47,8 +49,12 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme private Resources resources; private AppCompatActivity fragmentActivity; private LinearLayoutCompat root; + private RecyclerLazyLoader lazyLoader; private MediaService mediaService; - private String postId; + private GraphQLService graphQLService; + private boolean isLoggedIn; + private String postId, endCursor; + private boolean isComment; private final ServiceCallback> cb = new ServiceCallback>() { @Override @@ -78,11 +84,44 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme } }; + private final ServiceCallback acb = new ServiceCallback() { + @Override + public void onSuccess(final GraphQLUserListFetchResponse result) { + endCursor = result.getNextMaxId(); + final LikesAdapter likesAdapter = new LikesAdapter(result.getItems(), v -> { + final Object tag = v.getTag(); + if (tag instanceof ProfileModel) { + ProfileModel model = (ProfileModel) tag; + final Bundle bundle = new Bundle(); + bundle.putString("username", "@" + model.getUsername()); + NavHostFragment.findNavController(LikesViewerFragment.this).navigate(R.id.action_global_profileFragment, bundle); + } + }); + layoutManager = new LinearLayoutManager(getContext()); + binding.rvLikes.setAdapter(likesAdapter); + binding.rvLikes.setLayoutManager(layoutManager); + binding.swipeRefreshLayout.setRefreshing(false); + } + + @Override + public void onFailure(final Throwable t) { + Log.e(TAG, "Error", t); + try { + final Context context = getContext(); + Toast.makeText(context, t.getMessage(), Toast.LENGTH_SHORT).show(); + } + catch (Exception e) {} + } + }; + @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); + final String cookie = settingsHelper.getString(Constants.COOKIE); + isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != null; fragmentActivity = (AppCompatActivity) getActivity(); - mediaService = MediaService.getInstance(); + mediaService = isLoggedIn ? MediaService.getInstance() : null; + graphQLService = isLoggedIn ? null : GraphQLService.getInstance(); // setHasOptionsMenu(true); } @@ -103,16 +142,29 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme @Override public void onRefresh() { - mediaService.fetchLikes(postId, cb); + if (isComment && !isLoggedIn) { + lazyLoader.resetState(); + graphQLService.fetchCommentLikers(postId, null, acb); + } + else mediaService.fetchLikes(postId, isComment, cb); } private void init() { if (getArguments() == null) return; final LikesViewerFragmentArgs fragmentArgs = LikesViewerFragmentArgs.fromBundle(getArguments()); postId = fragmentArgs.getPostId(); + isComment = fragmentArgs.getIsComment(); binding.swipeRefreshLayout.setOnRefreshListener(this); binding.swipeRefreshLayout.setRefreshing(true); resources = getResources(); + if (isComment && !isLoggedIn) { + lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> { + if (!TextUtils.isEmpty(endCursor)) + graphQLService.fetchCommentLikers(postId, null, acb); + endCursor = null; + }); + binding.rvLikes.addOnScrollListener(lazyLoader); + } onRefresh(); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java index 257398c3..8be2d3e0 100644 --- a/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/PostViewV2Fragment.java @@ -542,6 +542,7 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment { if (navController != null && isLoggedIn) { final Bundle bundle = new Bundle(); bundle.putString("postId", feedModel.getPostId()); + bundle.putBoolean("isComment", false); navController.navigate(R.id.action_global_likesViewerFragment, bundle); } else { diff --git a/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java b/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java index 84dc3291..745a102e 100644 --- a/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/MediaRepository.java @@ -15,8 +15,9 @@ public interface MediaRepository { @GET("/api/v1/media/{mediaId}/info/") Call fetch(@Path("mediaId") final String mediaId); - @GET("/api/v1/media/{mediaId}/likers/") - Call fetchLikes(@Path("mediaId") final String mediaId); + @GET("/api/v1/media/{mediaId}/{action}/") + Call fetchLikes(@Path("mediaId") final String mediaId, + @Path("action") final String action); // one of "likers" or "comment_likers" @FormUrlEncoded @POST("/api/v1/media/{mediaId}/{action}/") diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/GraphQLUserListFetchResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/GraphQLUserListFetchResponse.java new file mode 100644 index 00000000..ebcd93b6 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/GraphQLUserListFetchResponse.java @@ -0,0 +1,79 @@ +package awais.instagrabber.repositories.responses; + +import androidx.annotation.NonNull; + +import java.util.List; +import java.util.Objects; + +import awais.instagrabber.models.ProfileModel; +import awais.instagrabber.utils.TextUtils; + +public class GraphQLUserListFetchResponse { + private String nextMaxId; + private String status; + private List items; + + public GraphQLUserListFetchResponse(final String nextMaxId, + final String status, + final List items) { + this.nextMaxId = nextMaxId; + this.status = status; + this.items = items; + } + + public boolean isMoreAvailable() { + return !TextUtils.isEmpty(nextMaxId); + } + + public String getNextMaxId() { + return nextMaxId; + } + + public GraphQLUserListFetchResponse setNextMaxId(final String nextMaxId) { + this.nextMaxId = nextMaxId; + return this; + } + + public String getStatus() { + return status; + } + + public GraphQLUserListFetchResponse setStatus(final String status) { + this.status = status; + return this; + } + + public List getItems() { + return items; + } + + public GraphQLUserListFetchResponse setItems(final List items) { + this.items = items; + return this; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final GraphQLUserListFetchResponse that = (GraphQLUserListFetchResponse) o; + return Objects.equals(nextMaxId, that.nextMaxId) && + Objects.equals(status, that.status) && + Objects.equals(items, that.items); + } + + @Override + public int hashCode() { + return Objects.hash(nextMaxId, status, items); + } + + @NonNull + @Override + public String toString() { + return "GraphQLUserListFetchResponse{" + + "nextMaxId='" + nextMaxId + '\'' + + ", status='" + status + '\'' + + ", items=" + items + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java index d21c7870..3a9c9606 100644 --- a/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java +++ b/app/src/main/java/awais/instagrabber/webservices/GraphQLService.java @@ -21,7 +21,9 @@ import java.util.List; import java.util.Map; import awais.instagrabber.models.FeedModel; +import awais.instagrabber.models.ProfileModel; import awais.instagrabber.repositories.GraphQLRepository; +import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse; import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.ResponseBodyUtils; @@ -181,4 +183,61 @@ public class GraphQLService extends BaseService { } return new PostsFetchResponse(feedModels, hasNextPage, endCursor); } + + public void fetchCommentLikers(final String commentId, + final String endCursor, + final ServiceCallback callback) { + final Map queryMap = new HashMap<>(); + queryMap.put("query_hash", "5f0b1f6281e72053cbc07909c8d154ae"); + queryMap.put("variables", "{\"comment_id\":\"" + commentId + "\"," + + "\"first\":30," + + "\"after\":\"" + (endCursor == null ? "" : endCursor) + "\"}"); + final Call request = repository.fetch(queryMap); + request.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, @NonNull final Response response) { + final String rawBody = response.body(); + if (rawBody == null) { + Log.e(TAG, "Error occurred while fetching gql comment likes of "+commentId); + callback.onSuccess(null); + return; + } + try { + final JSONObject body = new JSONObject(rawBody); + final String status = body.getString("status"); + final JSONObject data = body.getJSONObject("data").getJSONObject("comment").getJSONObject("edge_liked_by"); + final JSONObject pageInfo = data.getJSONObject("page_info"); + final String endCursor = pageInfo.getBoolean("has_next_page") ? pageInfo.getString("end_cursor") : null; + final JSONArray users = data.getJSONArray("edges"); + final int usersLen = users.length(); + final List userModels = new ArrayList<>(); + for (int j = 0; j < usersLen; ++j) { + final JSONObject userObject = users.getJSONObject(j).getJSONObject("node"); + userModels.add(new ProfileModel(userObject.optBoolean("is_private"), + false, + userObject.optBoolean("is_verified"), + userObject.getString("id"), + userObject.getString("username"), + userObject.optString("full_name"), + null, null, + userObject.getString("profile_pic_url"), + null, 0, 0, 0, false, false, false, false, false)); + } + callback.onSuccess(new GraphQLUserListFetchResponse(endCursor, status, userModels)); + } catch (JSONException e) { + Log.e(TAG, "onResponse", e); + if (callback != null) { + callback.onFailure(e); + } + } + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + if (callback != null) { + callback.onFailure(t); + } + } + }); + } } diff --git a/app/src/main/java/awais/instagrabber/webservices/MediaService.java b/app/src/main/java/awais/instagrabber/webservices/MediaService.java index 65c978dd..f21f7272 100644 --- a/app/src/main/java/awais/instagrabber/webservices/MediaService.java +++ b/app/src/main/java/awais/instagrabber/webservices/MediaService.java @@ -356,8 +356,9 @@ public class MediaService extends BaseService { } public void fetchLikes(final String mediaId, + final boolean isComment, @NonNull final ServiceCallback> callback) { - final Call likesRequest = repository.fetchLikes(mediaId); + final Call likesRequest = repository.fetchLikes(mediaId, isComment ? "comment_likers" : "likers"); likesRequest.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @NonNull final Response response) { diff --git a/app/src/main/res/navigation/comments_nav_graph.xml b/app/src/main/res/navigation/comments_nav_graph.xml index 91502f40..6f5e9bac 100644 --- a/app/src/main/res/navigation/comments_nav_graph.xml +++ b/app/src/main/res/navigation/comments_nav_graph.xml @@ -62,4 +62,19 @@ app:argType="string" app:nullable="false" /> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/direct_messages_nav_graph.xml b/app/src/main/res/navigation/direct_messages_nav_graph.xml index 87686c3d..a6846a39 100644 --- a/app/src/main/res/navigation/direct_messages_nav_graph.xml +++ b/app/src/main/res/navigation/direct_messages_nav_graph.xml @@ -68,10 +68,14 @@ - + + - + + diff --git a/app/src/main/res/navigation/feed_nav_graph.xml b/app/src/main/res/navigation/feed_nav_graph.xml index 85c17c3c..9a10f670 100644 --- a/app/src/main/res/navigation/feed_nav_graph.xml +++ b/app/src/main/res/navigation/feed_nav_graph.xml @@ -66,6 +66,10 @@ android:name="postId" app:argType="string" app:nullable="false" /> + diff --git a/app/src/main/res/navigation/hashtag_nav_graph.xml b/app/src/main/res/navigation/hashtag_nav_graph.xml index 6f7416e9..dab927b9 100644 --- a/app/src/main/res/navigation/hashtag_nav_graph.xml +++ b/app/src/main/res/navigation/hashtag_nav_graph.xml @@ -33,6 +33,10 @@ android:name="postId" app:argType="string" app:nullable="false" /> + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/location_nav_graph.xml b/app/src/main/res/navigation/location_nav_graph.xml index 100760b3..4c4bd2a0 100644 --- a/app/src/main/res/navigation/location_nav_graph.xml +++ b/app/src/main/res/navigation/location_nav_graph.xml @@ -33,6 +33,10 @@ android:name="postId" app:argType="string" app:nullable="false" /> + - + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/profile_nav_graph.xml b/app/src/main/res/navigation/profile_nav_graph.xml index 44ef7958..857331db 100644 --- a/app/src/main/res/navigation/profile_nav_graph.xml +++ b/app/src/main/res/navigation/profile_nav_graph.xml @@ -33,9 +33,12 @@ android:name="postId" app:argType="string" app:nullable="false" /> + - You can only download 100 posts at a time. Don\'t be too greedy! Copy username Copy comment + View comment likers Reply to comment Like comment Unlike comment