support for viewing comment likes

This commit is contained in:
Austin Huang 2020-12-26 13:14:08 -05:00
parent 49ba524305
commit 28af696e01
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
17 changed files with 304 additions and 25 deletions

View File

@ -25,6 +25,7 @@ import androidx.appcompat.widget.LinearLayoutCompat;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController;
import androidx.navigation.NavDirections; import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
@ -288,6 +289,7 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl
commentDialogList = new String[]{ commentDialogList = new String[]{
resources.getString(R.string.open_profile), 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),
resources.getString(R.string.comment_viewer_reply_comment), resources.getString(R.string.comment_viewer_reply_comment),
commentModel.getLiked() ? resources.getString(R.string.comment_viewer_unlike_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_like_comment),
@ -298,6 +300,7 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl
commentDialogList = new String[]{ commentDialogList = new String[]{
resources.getString(R.string.open_profile), 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),
resources.getString(R.string.comment_viewer_reply_comment), resources.getString(R.string.comment_viewer_reply_comment),
commentModel.getLiked() ? resources.getString(R.string.comment_viewer_unlike_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_like_comment),
@ -306,7 +309,8 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl
} else { } else {
commentDialogList = new String[]{ commentDialogList = new String[]{
resources.getString(R.string.open_profile), 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(); final Context context = getContext();
@ -321,7 +325,17 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl
case 1: // copy comment case 1: // copy comment
Utils.copyText(context, "@" + profileModel.getUsername() + ": " + commentModel.getText()); Utils.copyText(context, "@" + profileModel.getUsername() + ": " + commentModel.getText());
break; 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); commentsAdapter.setSelected(commentModel);
String mention = "@" + profileModel.getUsername() + " "; String mention = "@" + profileModel.getUsername() + " ";
binding.commentText.setText(mention); binding.commentText.setText(mention);
@ -333,7 +347,7 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl
imm.showSoftInput(binding.commentText, 0); imm.showSoftInput(binding.commentText, 0);
}, 200); }, 200);
break; break;
case 3: // like/unlike comment case 4: // like/unlike comment
if (csrfToken == null) { if (csrfToken == null) {
return; return;
} }
@ -373,7 +387,7 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl
} }
}); });
break; break;
case 4: // translate comment case 5: // translate comment
mediaService.translate(commentModel.getId(), "2", new ServiceCallback<String>() { mediaService.translate(commentModel.getId(), "2", new ServiceCallback<String>() {
@Override @Override
public void onSuccess(final String result) { public void onSuccess(final String result) {
@ -395,7 +409,7 @@ public final class CommentsViewerFragment extends BottomSheetDialogFragment impl
} }
}); });
break; break;
case 5: // delete comment case 6: // delete comment
final String userId = CookieUtils.getUserIdFromCookie(cookie); final String userId = CookieUtils.getUserIdFromCookie(cookie);
if (userId == null) return; if (userId == null) return;
mediaService.deleteComment( 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;
}
} }

View File

@ -14,28 +14,30 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.LinearLayoutCompat; import androidx.appcompat.widget.LinearLayoutCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import java.util.Collections;
import java.util.List; import java.util.List;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.R; import awais.instagrabber.R;
import awais.instagrabber.adapters.LikesAdapter; import awais.instagrabber.adapters.LikesAdapter;
import awais.instagrabber.customviews.helpers.RecyclerLazyLoader;
import awais.instagrabber.databinding.FragmentLikesBinding; import awais.instagrabber.databinding.FragmentLikesBinding;
import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.webservices.GraphQLService;
import awais.instagrabber.webservices.MediaService; import awais.instagrabber.webservices.MediaService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class LikesViewerFragment extends BottomSheetDialogFragment implements SwipeRefreshLayout.OnRefreshListener { public final class LikesViewerFragment extends BottomSheetDialogFragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "LikesViewerFragment"; private static final String TAG = "LikesViewerFragment";
@ -47,8 +49,12 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
private Resources resources; private Resources resources;
private AppCompatActivity fragmentActivity; private AppCompatActivity fragmentActivity;
private LinearLayoutCompat root; private LinearLayoutCompat root;
private RecyclerLazyLoader lazyLoader;
private MediaService mediaService; private MediaService mediaService;
private String postId; private GraphQLService graphQLService;
private boolean isLoggedIn;
private String postId, endCursor;
private boolean isComment;
private final ServiceCallback<List<ProfileModel>> cb = new ServiceCallback<List<ProfileModel>>() { private final ServiceCallback<List<ProfileModel>> cb = new ServiceCallback<List<ProfileModel>>() {
@Override @Override
@ -78,11 +84,44 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
} }
}; };
private final ServiceCallback<GraphQLUserListFetchResponse> acb = new ServiceCallback<GraphQLUserListFetchResponse>() {
@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 @Override
public void onCreate(@Nullable final Bundle savedInstanceState) { public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
final String cookie = settingsHelper.getString(Constants.COOKIE);
isLoggedIn = !TextUtils.isEmpty(cookie) && CookieUtils.getUserIdFromCookie(cookie) != null;
fragmentActivity = (AppCompatActivity) getActivity(); fragmentActivity = (AppCompatActivity) getActivity();
mediaService = MediaService.getInstance(); mediaService = isLoggedIn ? MediaService.getInstance() : null;
graphQLService = isLoggedIn ? null : GraphQLService.getInstance();
// setHasOptionsMenu(true); // setHasOptionsMenu(true);
} }
@ -103,16 +142,29 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
@Override @Override
public void onRefresh() { 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() { private void init() {
if (getArguments() == null) return; if (getArguments() == null) return;
final LikesViewerFragmentArgs fragmentArgs = LikesViewerFragmentArgs.fromBundle(getArguments()); final LikesViewerFragmentArgs fragmentArgs = LikesViewerFragmentArgs.fromBundle(getArguments());
postId = fragmentArgs.getPostId(); postId = fragmentArgs.getPostId();
isComment = fragmentArgs.getIsComment();
binding.swipeRefreshLayout.setOnRefreshListener(this); binding.swipeRefreshLayout.setOnRefreshListener(this);
binding.swipeRefreshLayout.setRefreshing(true); binding.swipeRefreshLayout.setRefreshing(true);
resources = getResources(); 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(); onRefresh();
} }
} }

View File

@ -542,6 +542,7 @@ public class PostViewV2Fragment extends SharedElementTransitionDialogFragment {
if (navController != null && isLoggedIn) { if (navController != null && isLoggedIn) {
final Bundle bundle = new Bundle(); final Bundle bundle = new Bundle();
bundle.putString("postId", feedModel.getPostId()); bundle.putString("postId", feedModel.getPostId());
bundle.putBoolean("isComment", false);
navController.navigate(R.id.action_global_likesViewerFragment, bundle); navController.navigate(R.id.action_global_likesViewerFragment, bundle);
} }
else { else {

View File

@ -15,8 +15,9 @@ public interface MediaRepository {
@GET("/api/v1/media/{mediaId}/info/") @GET("/api/v1/media/{mediaId}/info/")
Call<String> fetch(@Path("mediaId") final String mediaId); Call<String> fetch(@Path("mediaId") final String mediaId);
@GET("/api/v1/media/{mediaId}/likers/") @GET("/api/v1/media/{mediaId}/{action}/")
Call<String> fetchLikes(@Path("mediaId") final String mediaId); Call<String> fetchLikes(@Path("mediaId") final String mediaId,
@Path("action") final String action); // one of "likers" or "comment_likers"
@FormUrlEncoded @FormUrlEncoded
@POST("/api/v1/media/{mediaId}/{action}/") @POST("/api/v1/media/{mediaId}/{action}/")

View File

@ -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<ProfileModel> items;
public GraphQLUserListFetchResponse(final String nextMaxId,
final String status,
final List<ProfileModel> 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<ProfileModel> getItems() {
return items;
}
public GraphQLUserListFetchResponse setItems(final List<ProfileModel> 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 +
'}';
}
}

View File

@ -21,7 +21,9 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.FeedModel;
import awais.instagrabber.models.ProfileModel;
import awais.instagrabber.repositories.GraphQLRepository; import awais.instagrabber.repositories.GraphQLRepository;
import awais.instagrabber.repositories.responses.GraphQLUserListFetchResponse;
import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.ResponseBodyUtils;
@ -181,4 +183,61 @@ public class GraphQLService extends BaseService {
} }
return new PostsFetchResponse(feedModels, hasNextPage, endCursor); return new PostsFetchResponse(feedModels, hasNextPage, endCursor);
} }
public void fetchCommentLikers(final String commentId,
final String endCursor,
final ServiceCallback<GraphQLUserListFetchResponse> callback) {
final Map<String, String> queryMap = new HashMap<>();
queryMap.put("query_hash", "5f0b1f6281e72053cbc07909c8d154ae");
queryMap.put("variables", "{\"comment_id\":\"" + commentId + "\"," +
"\"first\":30," +
"\"after\":\"" + (endCursor == null ? "" : endCursor) + "\"}");
final Call<String> request = repository.fetch(queryMap);
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 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<ProfileModel> 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<String> call, @NonNull final Throwable t) {
if (callback != null) {
callback.onFailure(t);
}
}
});
}
} }

View File

@ -356,8 +356,9 @@ public class MediaService extends BaseService {
} }
public void fetchLikes(final String mediaId, public void fetchLikes(final String mediaId,
final boolean isComment,
@NonNull final ServiceCallback<List<ProfileModel>> callback) { @NonNull final ServiceCallback<List<ProfileModel>> callback) {
final Call<String> likesRequest = repository.fetchLikes(mediaId); final Call<String> likesRequest = repository.fetchLikes(mediaId, isComment ? "comment_likers" : "likers");
likesRequest.enqueue(new Callback<String>() { likesRequest.enqueue(new Callback<String>() {
@Override @Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {

View File

@ -62,4 +62,19 @@
app:argType="string" app:argType="string"
app:nullable="false" /> app:nullable="false" />
</action> </action>
<include app:graph="@navigation/likes_nav_graph" />
<action
android:id="@+id/action_global_likesViewerFragment"
app:destination="@id/likes_nav_graph">
<argument
android:name="postId"
app:argType="string"
app:nullable="false" />
<argument
android:name="isComment"
app:argType="boolean"
app:nullable="false" />
</action>
</navigation> </navigation>

View File

@ -72,6 +72,10 @@
android:name="postId" android:name="postId"
app:argType="string" app:argType="string"
app:nullable="false" /> app:nullable="false" />
<argument
android:name="isComment"
app:argType="boolean"
app:nullable="false" />
</action> </action>
<fragment <fragment

View File

@ -66,6 +66,10 @@
android:name="postId" android:name="postId"
app:argType="string" app:argType="string"
app:nullable="false" /> app:nullable="false" />
<argument
android:name="isComment"
app:argType="boolean"
app:nullable="false" />
</action> </action>
<include app:graph="@navigation/notification_viewer_nav_graph" /> <include app:graph="@navigation/notification_viewer_nav_graph" />

View File

@ -66,6 +66,10 @@
android:name="postId" android:name="postId"
app:argType="string" app:argType="string"
app:nullable="false" /> app:nullable="false" />
<argument
android:name="isComment"
app:argType="boolean"
app:nullable="false" />
</action> </action>
<include app:graph="@navigation/notification_viewer_nav_graph" /> <include app:graph="@navigation/notification_viewer_nav_graph" />

View File

@ -33,6 +33,10 @@
android:name="postId" android:name="postId"
app:argType="string" app:argType="string"
app:nullable="false" /> app:nullable="false" />
<argument
android:name="isComment"
app:argType="boolean"
app:nullable="false" />
</action> </action>
<action <action

View File

@ -27,6 +27,10 @@
android:name="postId" android:name="postId"
app:argType="string" app:argType="string"
app:nullable="false" /> app:nullable="false" />
<argument
android:name="isComment"
app:argType="boolean"
app:nullable="false" />
</dialog> </dialog>
<action <action
@ -36,5 +40,9 @@
android:name="postId" android:name="postId"
app:argType="string" app:argType="string"
app:nullable="false" /> app:nullable="false" />
<argument
android:name="isComment"
app:argType="boolean"
app:nullable="false" />
</action> </action>
</navigation> </navigation>

View File

@ -33,6 +33,10 @@
android:name="postId" android:name="postId"
app:argType="string" app:argType="string"
app:nullable="false" /> app:nullable="false" />
<argument
android:name="isComment"
app:argType="boolean"
app:nullable="false" />
</action> </action>
<action <action

View File

@ -16,7 +16,6 @@
app:destination="@id/notificationsViewer" /> app:destination="@id/notificationsViewer" />
<include app:graph="@navigation/comments_nav_graph" /> <include app:graph="@navigation/comments_nav_graph" />
<include app:graph="@navigation/likes_nav_graph" />
<action <action
android:id="@+id/action_global_commentsViewerFragment" android:id="@+id/action_global_commentsViewerFragment"
@ -34,4 +33,19 @@
app:argType="string" app:argType="string"
app:nullable="false" /> app:nullable="false" />
</action> </action>
<include app:graph="@navigation/likes_nav_graph" />
<action
android:id="@+id/action_global_likesViewerFragment"
app:destination="@id/likes_nav_graph">
<argument
android:name="postId"
app:argType="string"
app:nullable="false" />
<argument
android:name="isComment"
app:argType="boolean"
app:nullable="false" />
</action>
</navigation> </navigation>

View File

@ -33,9 +33,12 @@
android:name="postId" android:name="postId"
app:argType="string" app:argType="string"
app:nullable="false" /> app:nullable="false" />
<argument
android:name="isComment"
app:argType="boolean"
app:nullable="false" />
</action> </action>
<include app:graph="@navigation/hashtag_nav_graph" /> <include app:graph="@navigation/hashtag_nav_graph" />
<action <action

View File

@ -195,6 +195,7 @@
<string name="downloader_too_many">You can only download 100 posts at a time. Don\'t be too greedy!</string> <string name="downloader_too_many">You can only download 100 posts at a time. Don\'t be too greedy!</string>
<string name="comment_viewer_copy_user">Copy username</string> <string name="comment_viewer_copy_user">Copy username</string>
<string name="comment_viewer_copy_comment">Copy comment</string> <string name="comment_viewer_copy_comment">Copy comment</string>
<string name="comment_viewer_see_likers">View comment likers</string>
<string name="comment_viewer_reply_comment">Reply to comment</string> <string name="comment_viewer_reply_comment">Reply to comment</string>
<string name="comment_viewer_like_comment">Like comment</string> <string name="comment_viewer_like_comment">Like comment</string>
<string name="comment_viewer_unlike_comment">Unlike comment</string> <string name="comment_viewer_unlike_comment">Unlike comment</string>