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.ChildCommentsFetchResponse; import awais.instagrabber.repositories.responses.CommentsFetchResponse; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CoroutineUtilsKt; import awais.instagrabber.utils.Utils; import awais.instagrabber.webservices.CommentService; import awais.instagrabber.webservices.GraphQLRepository; import awais.instagrabber.webservices.ServiceCallback; import kotlin.coroutines.Continuation; import kotlinx.coroutines.Dispatchers; import static awais.instagrabber.utils.Utils.settingsHelper; public class CommentsViewerViewModel extends ViewModel { private static final String TAG = CommentsViewerViewModel.class.getSimpleName(); private final MutableLiveData isLoggedIn = new MutableLiveData<>(false); private final MutableLiveData currentUserId = new MutableLiveData<>(0L); private final MutableLiveData>> rootList = new MutableLiveData<>(); private final MutableLiveData rootCount = new MutableLiveData<>(0); private final MutableLiveData>> replyList = new MutableLiveData<>(); private final GraphQLRepository graphQLRepository; private String shortCode; private String postId; private String rootCursor; private boolean rootHasNext = true; private Comment repliesParent, replyTo; private String repliesCursor; private boolean repliesHasNext = true; private final CommentService commentService; private List prevReplies; private String prevRepliesCursor; private boolean prevRepliesHasNext = true; private final ServiceCallback ccb = new ServiceCallback() { @Override public void onSuccess(final CommentsFetchResponse result) { // Log.d(TAG, "onSuccess: " + result); if (result == null) { rootList.postValue(Resource.error(R.string.generic_null_response, getPrevList(rootList))); return; } List comments = result.getComments(); if (rootCursor == null) { rootCount.postValue(result.getCommentCount()); } if (rootCursor != null) { comments = mergeList(rootList, comments); } rootCursor = result.getNextMinId(); rootHasNext = result.getHasMoreComments(); rootList.postValue(Resource.success(comments)); } @Override public void onFailure(final Throwable t) { Log.e(TAG, "onFailure: ", t); rootList.postValue(Resource.error(t.getMessage(), getPrevList(rootList))); } }; private final ServiceCallback rcb = new ServiceCallback() { @Override public void onSuccess(final ChildCommentsFetchResponse result) { // Log.d(TAG, "onSuccess: " + result); if (result == null) { rootList.postValue(Resource.error(R.string.generic_null_response, getPrevList(replyList))); return; } List comments = result.getChildComments(); // Replies if (repliesCursor == null) { // add parent to top of replies comments = ImmutableList.builder() .add(repliesParent) .addAll(comments) .build(); } if (repliesCursor != null) { comments = mergeList(replyList, comments); } repliesCursor = result.getNextMaxChildCursor(); repliesHasNext = result.getHasMoreTailChildComments(); replyList.postValue(Resource.success(comments)); } @Override public void onFailure(final Throwable t) { Log.e(TAG, "onFailure: ", t); replyList.postValue(Resource.error(t.getMessage(), getPrevList(replyList))); } }; public CommentsViewerViewModel() { graphQLRepository = GraphQLRepository.Companion.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); commentService = CommentService.getInstance(deviceUuid, csrfToken, userIdFromCookie); } public void setCurrentUser(final User currentUser) { isLoggedIn.postValue(currentUser != null); currentUserId.postValue(currentUser == null ? 0 : currentUser.getPk()); } public void setPostDetails(final String shortCode, final String postId, final long postUserId) { this.shortCode = shortCode; this.postId = postId; fetchComments(); } public LiveData isLoggedIn() { return isLoggedIn; } public LiveData getCurrentUserId() { return currentUserId; } @Nullable public Comment getRepliesParent() { return repliesParent; } @Nullable public void setReplyTo(final Comment replyTo) { this.replyTo = replyTo; } public LiveData>> getRootList() { return rootList; } public LiveData>> getReplyList() { return replyList; } public LiveData getRootCommentsCount() { return rootCount; } public void fetchComments() { if (shortCode == null || postId == null) return; if (!rootHasNext) return; rootList.postValue(Resource.loading(getPrevList(rootList))); if (isLoggedIn.getValue()) { commentService.fetchComments(postId, rootCursor, ccb); return; } graphQLRepository.fetchComments( shortCode, true, rootCursor, enqueueRequest(true, shortCode, ccb) ); } public void fetchReplies() { if (repliesParent == null) return; fetchReplies(repliesParent.getPk()); } public void fetchReplies(@NonNull final String commentId) { if (!repliesHasNext) return; final List list; if (repliesParent != null && !Objects.equals(repliesParent.getPk(), commentId)) { repliesCursor = null; repliesHasNext = false; list = Collections.emptyList(); } else { list = getPrevList(replyList); } replyList.postValue(Resource.loading(list)); final Boolean isLoggedInValue = isLoggedIn.getValue(); if (isLoggedInValue != null && isLoggedInValue) { commentService.fetchChildComments(postId, commentId, repliesCursor, rcb); return; } graphQLRepository.fetchComments(commentId, false, repliesCursor, enqueueRequest(false, commentId, rcb)); } private Continuation enqueueRequest(final boolean root, final String shortCodeOrCommentId, @SuppressWarnings("rawtypes") final ServiceCallback callback) { return CoroutineUtilsKt.getContinuation((response, throwable) -> { if (throwable != null) { callback.onFailure(throwable); return; } if (response == null) { Log.e(TAG, "Error occurred while fetching gql comments of " + shortCodeOrCommentId); //noinspection unchecked callback.onSuccess(null); return; } try { final JSONObject body = root ? new JSONObject(response).getJSONObject("data") .getJSONObject("shortcode_media") .getJSONObject("edge_media_to_parent_comment") : new JSONObject(response).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") || !hasNextPage ? null : pageInfo.optString("end_cursor"); final JSONArray commentsJsonArray = body.getJSONArray("edges"); final ImmutableList.Builder builder = ImmutableList.builder(); for (int i = 0; i < commentsJsonArray.length(); i++) { final Comment commentModel = getComment(commentsJsonArray.getJSONObject(i).getJSONObject("node"), root); builder.add(commentModel); } final Object result = root ? new CommentsFetchResponse(count, endCursor, builder.build(), hasNextPage) : new ChildCommentsFetchResponse(count, endCursor, builder.build(), hasNextPage); //noinspection unchecked callback.onSuccess(result); } catch (Exception e) { Log.e(TAG, "onResponse", e); callback.onFailure(e); } }, Dispatchers.getIO()); } @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"), owner.optBoolean("is_verified")); 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); } @NonNull private List getPrevList(@NonNull final LiveData>> list) { if (list.getValue() == null) return Collections.emptyList(); final Resource> listResource = list.getValue(); if (listResource.data == null) return Collections.emptyList(); return listResource.data; } private List mergeList(@NonNull final LiveData>> list, final List comments) { final List prevList = getPrevList(list); if (comments == null) { return prevList; } return ImmutableList.builder() .addAll(prevList) .addAll(comments) .build(); } public void showReplies(final Comment comment) { if (comment == null) return; if (repliesParent == null || !Objects.equals(repliesParent.getPk(), comment.getPk())) { repliesParent = comment; replyTo = comment; prevReplies = null; prevRepliesCursor = null; prevRepliesHasNext = true; fetchReplies(comment.getPk()); 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.getPk()); } public LiveData> likeComment(@NonNull final Comment comment, final boolean liked, final boolean isReply) { final MutableLiveData> data = new MutableLiveData<>(Resource.loading(null)); final ServiceCallback callback = new ServiceCallback() { @Override public void onSuccess(final Boolean result) { if (result == null || !result) { data.postValue(Resource.error(R.string.downloader_unknown_error, null)); return; } data.postValue(Resource.success(new Object())); setLiked(isReply, comment, liked); } @Override public void onFailure(final Throwable t) { Log.e(TAG, "Error liking comment", t); data.postValue(Resource.error(t.getMessage(), null)); } }; if (liked) { commentService.commentLike(comment.getPk(), callback); } else { commentService.commentUnlike(comment.getPk(), callback); } return data; } private void setLiked(final boolean isReply, @NonNull final Comment comment, final boolean liked) { final List list = getPrevList(isReply ? replyList : rootList); if (list == null) return; final List copy = new ArrayList<>(list); OptionalInt indexOpt = IntStream.range(0, copy.size()) .filter(i -> copy.get(i) != null && Objects.equals(copy.get(i).getPk(), comment.getPk())) .findFirst(); if (!indexOpt.isPresent()) return; try { final Comment clone = (Comment) comment.clone(); clone.setLiked(liked); copy.set(indexOpt.getAsInt(), clone); final MutableLiveData>> liveData = isReply ? replyList : rootList; liveData.postValue(Resource.success(copy)); } catch (Exception e) { Log.e(TAG, "setLiked: ", e); } } public LiveData> comment(@NonNull final String text, final boolean isReply) { final MutableLiveData> data = new MutableLiveData<>(Resource.loading(null)); String replyToId = null; if (isReply && replyTo != null) { replyToId = replyTo.getPk(); } if (isReply && replyToId == null) { data.postValue(Resource.error(null, null)); return data; } commentService.comment(postId, text, replyToId, new ServiceCallback() { @Override public void onSuccess(final Comment result) { if (result == null) { data.postValue(Resource.error(R.string.downloader_unknown_error, null)); return; } addComment(result, isReply); data.postValue(Resource.success(new Object())); } @Override public void onFailure(final Throwable t) { Log.e(TAG, "Error during comment", t); data.postValue(Resource.error(t.getMessage(), null)); } }); return data; } private void addComment(@NonNull final Comment comment, final boolean isReply) { final List list = getPrevList(isReply ? replyList : rootList); final ImmutableList.Builder builder = ImmutableList.builder(); if (isReply) { // replies are added to the bottom of the list to preserve chronological order builder.addAll(list) .add(comment); } else { builder.add(comment) .addAll(list); } final MutableLiveData>> liveData = isReply ? replyList : rootList; liveData.postValue(Resource.success(builder.build())); } public void translate(@NonNull final Comment comment, @NonNull final ServiceCallback callback) { commentService.translate(comment.getPk(), callback); } public LiveData> deleteComment(@NonNull final Comment comment, final boolean isReply) { final MutableLiveData> data = new MutableLiveData<>(Resource.loading(null)); commentService.deleteComment(postId, comment.getPk(), new ServiceCallback() { @Override public void onSuccess(final Boolean result) { if (result == null || !result) { data.postValue(Resource.error(R.string.downloader_unknown_error, null)); return; } removeComment(comment, isReply); data.postValue(Resource.success(new Object())); } @Override public void onFailure(final Throwable t) { Log.e(TAG, "Error deleting comment", t); data.postValue(Resource.error(t.getMessage(), null)); } }); return data; } private void removeComment(@NonNull final Comment comment, final boolean isReply) { final List list = getPrevList(isReply ? replyList : rootList); final List updated = list.stream() .filter(Objects::nonNull) .filter(c -> !Objects.equals(c.getPk(), comment.getPk())) .collect(Collectors.toList()); final MutableLiveData>> liveData = isReply ? replyList : rootList; liveData.postValue(Resource.success(updated)); } public void clearReplies() { prevRepliesCursor = repliesCursor; prevRepliesHasNext = repliesHasNext; repliesCursor = null; repliesHasNext = true; // cache prev reply list to save time and data if user clicks same comment again prevReplies = getPrevList(replyList); replyList.postValue(Resource.success(Collections.emptyList())); } }