Improve comments viewer ui and ux. Update ui for likes viewer and follows viewer.

This commit is contained in:
Ammar Githam 2021-05-02 18:16:25 +09:00
parent 18292ead4e
commit 074ee18c9d
45 changed files with 2147 additions and 1554 deletions

View File

@ -1,195 +1,60 @@
package awais.instagrabber.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import awais.instagrabber.adapters.viewholder.comments.ChildCommentViewHolder;
import awais.instagrabber.adapters.viewholder.comments.ParentCommentViewHolder;
import awais.instagrabber.adapters.viewholder.CommentViewHolder;
import awais.instagrabber.databinding.ItemCommentBinding;
import awais.instagrabber.databinding.ItemCommentSmallBinding;
import awais.instagrabber.models.CommentModel;
import awais.instagrabber.models.Comment;
public final class CommentsAdapter extends ListAdapter<CommentModel, RecyclerView.ViewHolder> {
private static final int TYPE_PARENT = 1;
private static final int TYPE_CHILD = 2;
private final Map<Integer, Integer> positionTypeMap = new HashMap<>();
// private final Filter filter = new Filter() {
// @NonNull
// @Override
// protected FilterResults performFiltering(final CharSequence filter) {
// final FilterResults results = new FilterResults();
// results.values = commentModels;
//
// final int commentsLen = commentModels == null ? 0 : commentModels.size();
// if (commentModels != null && commentsLen > 0 && !TextUtils.isEmpty(filter)) {
// final String query = filter.toString().toLowerCase();
// final ArrayList<CommentModel> filterList = new ArrayList<>(commentsLen);
//
// for (final CommentModel commentModel : commentModels) {
// final String commentText = commentModel.getText().toString().toLowerCase();
//
// if (commentText.contains(query)) filterList.add(commentModel);
// else {
// final List<CommentModel> childCommentModels = commentModel.getChildCommentModels();
// if (childCommentModels != null) {
// for (final CommentModel childCommentModel : childCommentModels) {
// final String childCommentText = childCommentModel.getText().toString().toLowerCase();
// if (childCommentText.contains(query)) filterList.add(commentModel);
// }
// }
// }
// }
// filterList.trimToSize();
// results.values = filterList.toArray(new CommentModel[0]);
// }
//
// return results;
// }
//
// @Override
// protected void publishResults(final CharSequence constraint, @NonNull final FilterResults results) {
// if (results.values instanceof List) {
// //noinspection unchecked
// filteredCommentModels = (List<CommentModel>) results.values;
// notifyDataSetChanged();
// }
// }
// };
private static final DiffUtil.ItemCallback<CommentModel> DIFF_CALLBACK = new DiffUtil.ItemCallback<CommentModel>() {
public final class CommentsAdapter extends ListAdapter<Comment, CommentViewHolder> {
private static final DiffUtil.ItemCallback<Comment> DIFF_CALLBACK = new DiffUtil.ItemCallback<Comment>() {
@Override
public boolean areItemsTheSame(@NonNull final CommentModel oldItem, @NonNull final CommentModel newItem) {
return oldItem.getId().equals(newItem.getId());
public boolean areItemsTheSame(@NonNull final Comment oldItem, @NonNull final Comment newItem) {
return Objects.equals(oldItem.getId(), newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final CommentModel oldItem, @NonNull final CommentModel newItem) {
return oldItem.getId().equals(newItem.getId());
public boolean areContentsTheSame(@NonNull final Comment oldItem, @NonNull final Comment newItem) {
return Objects.equals(oldItem, newItem);
}
};
private final CommentCallback commentCallback;
private CommentModel selected, toChangeLike;
private int selectedIndex, likedIndex;
public CommentsAdapter(final CommentCallback commentCallback) {
private final boolean showingReplies;
private final CommentCallback commentCallback;
private final long currentUserId;
public CommentsAdapter(final long currentUserId,
final boolean showingReplies,
final CommentCallback commentCallback) {
super(DIFF_CALLBACK);
this.showingReplies = showingReplies;
this.currentUserId = currentUserId;
this.commentCallback = commentCallback;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final Context context = parent.getContext();
final LayoutInflater layoutInflater = LayoutInflater.from(context);
if (type == TYPE_PARENT) {
final ItemCommentBinding binding = ItemCommentBinding.inflate(layoutInflater, parent, false);
return new ParentCommentViewHolder(binding);
}
final ItemCommentSmallBinding binding = ItemCommentSmallBinding.inflate(layoutInflater, parent, false);
return new ChildCommentViewHolder(binding);
public CommentViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemCommentBinding binding = ItemCommentBinding.inflate(layoutInflater, parent, false);
return new CommentViewHolder(binding, currentUserId, commentCallback);
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
CommentModel commentModel = getItem(position);
if (commentModel == null) return;
final int type = getItemViewType(position);
final boolean selected = this.selected != null && this.selected.getId().equals(commentModel.getId());
final boolean toLike = this.toChangeLike != null && this.toChangeLike.getId().equals(commentModel.getId());
if (toLike) commentModel = this.toChangeLike;
if (type == TYPE_PARENT) {
final ParentCommentViewHolder viewHolder = (ParentCommentViewHolder) holder;
viewHolder.bind(commentModel, selected, commentCallback);
return;
}
final ChildCommentViewHolder viewHolder = (ChildCommentViewHolder) holder;
viewHolder.bind(commentModel, selected, commentCallback);
}
@Override
public void submitList(@Nullable final List<CommentModel> list) {
final List<CommentModel> flatList = flattenList(list);
super.submitList(flatList);
}
@Override
public void submitList(@Nullable final List<CommentModel> list, @Nullable final Runnable commitCallback) {
final List<CommentModel> flatList = flattenList(list);
super.submitList(flatList, commitCallback);
}
private List<CommentModel> flattenList(final List<CommentModel> list) {
if (list == null) {
return Collections.emptyList();
}
final List<CommentModel> flatList = new ArrayList<>();
int lastCommentIndex = -1;
for (final CommentModel parent : list) {
lastCommentIndex++;
flatList.add(parent);
positionTypeMap.put(lastCommentIndex, TYPE_PARENT);
final List<CommentModel> children = parent.getChildCommentModels();
if (children != null) {
for (final CommentModel child : children) {
lastCommentIndex++;
flatList.add(child);
positionTypeMap.put(lastCommentIndex, TYPE_CHILD);
}
}
}
return flatList;
}
@Override
public int getItemViewType(final int position) {
final Integer type = positionTypeMap.get(position);
if (type == null) {
return TYPE_PARENT;
}
return type;
}
public void setSelected(final CommentModel commentModel) {
this.selected = commentModel;
selectedIndex = getCurrentList().indexOf(commentModel);
notifyItemChanged(selectedIndex);
}
public void clearSelection() {
this.selected = null;
notifyItemChanged(selectedIndex);
}
public void setLiked(final CommentModel commentModel, final boolean liked) {
likedIndex = getCurrentList().indexOf(commentModel);
CommentModel newCommentModel = commentModel;
newCommentModel.setLiked(liked);
this.toChangeLike = newCommentModel;
notifyItemChanged(likedIndex);
}
public CommentModel getSelected() {
return selected;
public void onBindViewHolder(@NonNull final CommentViewHolder holder, final int position) {
final Comment comment = getItem(position);
holder.bind(comment, showingReplies && position == 0, showingReplies && position != 0);
}
public interface CommentCallback {
void onClick(final CommentModel comment);
void onClick(final Comment comment);
void onHashtagClick(final String hashtag);
@ -198,5 +63,15 @@ public final class CommentsAdapter extends ListAdapter<CommentModel, RecyclerVie
void onURLClick(final String url);
void onEmailClick(final String emailAddress);
void onLikeClick(final Comment comment, boolean liked, final boolean isReply);
void onRepliesClick(final Comment comment);
void onViewLikes(Comment comment);
void onTranslate(Comment comment);
void onDelete(Comment comment, boolean isReply);
}
}

View File

@ -34,7 +34,7 @@ public final class LikesAdapter extends RecyclerView.Adapter<FollowsViewHolder>
@Override
public void onBindViewHolder(@NonNull final FollowsViewHolder holder, final int position) {
final User model = profileModels.get(position);
holder.bind(model, null, onClickListener);
holder.bind(model, onClickListener);
}
@Override

View File

@ -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);
// });
// }
}

View File

@ -2,10 +2,9 @@ package awais.instagrabber.adapters.viewholder;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.models.FollowModel;
import awais.instagrabber.repositories.responses.User;
@ -14,23 +13,19 @@ public final class FollowsViewHolder extends RecyclerView.ViewHolder {
private final ItemFollowBinding binding;
public FollowsViewHolder(final ItemFollowBinding binding) {
public FollowsViewHolder(@NonNull final ItemFollowBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final User model,
final List<Long> admins,
final View.OnClickListener onClickListener) {
if (model == null) return;
itemView.setTag(model);
itemView.setOnClickListener(onClickListener);
binding.tvUsername.setText(model.getUsername());
binding.tvFullName.setText(model.getFullName());
if (admins != null && admins.contains(model.getPk())) {
binding.isAdmin.setVisibility(View.VISIBLE);
}
binding.ivProfilePic.setImageURI(model.getProfilePicUrl());
binding.username.setUsername("@" + model.getUsername(), model.isVerified());
binding.fullName.setText(model.getFullName());
binding.profilePic.setImageURI(model.getProfilePicUrl());
}
public void bind(final FollowModel model,
@ -38,8 +33,8 @@ public final class FollowsViewHolder extends RecyclerView.ViewHolder {
if (model == null) return;
itemView.setTag(model);
itemView.setOnClickListener(onClickListener);
binding.tvUsername.setText(model.getUsername());
binding.tvFullName.setText(model.getFullName());
binding.ivProfilePic.setImageURI(model.getProfilePicUrl());
binding.username.setUsername("@" + model.getUsername());
binding.fullName.setText(model.getFullName());
binding.profilePic.setImageURI(model.getProfilePicUrl());
}
}

View File

@ -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));
}
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}

View File

@ -70,6 +70,9 @@ public final class ProfilePicView extends CircularImageView {
case SMALL:
dimenRes = R.dimen.profile_pic_size_small;
break;
case SMALLER:
dimenRes = R.dimen.profile_pic_size_smaller;
break;
case TINY:
dimenRes = R.dimen.profile_pic_size_tiny;
break;
@ -113,7 +116,8 @@ public final class ProfilePicView extends CircularImageView {
TINY(0),
SMALL(1),
REGULAR(2),
LARGE(3);
LARGE(3),
SMALLER(4);
private final int value;
private static final Map<Integer, Size> map = new HashMap<>();

View File

@ -1,10 +1,12 @@
package awais.instagrabber.customviews;
import android.content.Context;
import android.text.InputFilter;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.emoji.widget.EmojiTextViewHelper;
import java.util.ArrayList;
import java.util.List;
@ -23,6 +25,8 @@ public class RamboTextViewV2 extends AutoLinkTextView {
private final List<OnURLClickListener> onURLClickListeners = new ArrayList<>();
private final List<OnEmailClickListener> onEmailClickListeners = new ArrayList<>();
private EmojiTextViewHelper emojiTextViewHelper;
public RamboTextViewV2(@NonNull final Context context,
@Nullable final AttributeSet attrs) {
super(context, attrs);
@ -30,6 +34,7 @@ public class RamboTextViewV2 extends AutoLinkTextView {
}
private void init() {
getEmojiTextViewHelper().updateTransformationMethod();
addAutoLinkMode(MODE_HASHTAG.INSTANCE, MODE_MENTION.INSTANCE, MODE_EMAIL.INSTANCE, MODE_URL.INSTANCE);
onAutoLinkClick(autoLinkItem -> {
final Mode mode = autoLinkItem.getMode();
@ -60,6 +65,25 @@ public class RamboTextViewV2 extends AutoLinkTextView {
onAutoLinkLongClick(autoLinkItem -> {});
}
@Override
public void setFilters(InputFilter[] filters) {
super.setFilters(getEmojiTextViewHelper().getFilters(filters));
}
@Override
public void setAllCaps(boolean allCaps) {
super.setAllCaps(allCaps);
getEmojiTextViewHelper().setAllCaps(allCaps);
}
private EmojiTextViewHelper getEmojiTextViewHelper() {
if (emojiTextViewHelper == null) {
emojiTextViewHelper = new EmojiTextViewHelper(this);
}
return emojiTextViewHelper;
}
public void addOnMentionClickListener(final OnMentionClickListener onMentionClickListener) {
if (onMentionClickListener == null) {
return;

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -11,6 +11,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
@ -34,7 +35,7 @@ import awais.instagrabber.webservices.ServiceCallback;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class LikesViewerFragment extends BottomSheetDialogFragment implements SwipeRefreshLayout.OnRefreshListener {
private static final String TAG = "LikesViewerFragment";
private static final String TAG = LikesViewerFragment.class.getSimpleName();
private FragmentLikesBinding binding;
private RecyclerLazyLoader lazyLoader;
@ -58,6 +59,7 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
});
binding.rvLikes.setAdapter(likesAdapter);
binding.rvLikes.setLayoutManager(new LinearLayoutManager(getContext()));
binding.rvLikes.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL));
binding.swipeRefreshLayout.setRefreshing(false);
}
@ -71,7 +73,7 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
}
};
private final ServiceCallback<GraphQLUserListFetchResponse> acb = new ServiceCallback<GraphQLUserListFetchResponse>() {
private final ServiceCallback<GraphQLUserListFetchResponse> anonCb = new ServiceCallback<GraphQLUserListFetchResponse>() {
@Override
public void onSuccess(final GraphQLUserListFetchResponse result) {
endCursor = result.getNextMaxId();
@ -127,7 +129,7 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
public void onRefresh() {
if (isComment && !isLoggedIn) {
lazyLoader.resetState();
graphQLService.fetchCommentLikers(postId, null, acb);
graphQLService.fetchCommentLikers(postId, null, anonCb);
} else mediaService.fetchLikes(postId, isComment, cb);
}
@ -141,9 +143,10 @@ public final class LikesViewerFragment extends BottomSheetDialogFragment impleme
if (isComment && !isLoggedIn) {
final LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
binding.rvLikes.setLayoutManager(layoutManager);
binding.rvLikes.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.HORIZONTAL));
lazyLoader = new RecyclerLazyLoader(layoutManager, (page, totalItemsCount) -> {
if (!TextUtils.isEmpty(endCursor))
graphQLService.fetchCommentLikers(postId, endCursor, acb);
graphQLService.fetchCommentLikers(postId, endCursor, anonCb);
endCursor = null;
});
binding.rvLikes.addOnScrollListener(lazyLoader);

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -514,7 +514,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
super.onDestroy();
}
@SuppressLint("UnsafeExperimentalUsageError")
@SuppressLint("UnsafeOptInUsageError")
private void cleanup() {
if (prevTitleRunnable != null) {
appExecutors.mainThread().cancel(prevTitleRunnable);
@ -840,7 +840,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact
}
}
@SuppressLint("UnsafeExperimentalUsageError")
@SuppressLint("UnsafeOptInUsageError")
private void attachPendingRequestsBadge(@Nullable final Integer count) {
if (pendingRequestCountBadgeDrawable == null) {
final Context context = getContext();

View File

@ -111,12 +111,16 @@ public class FeedFragment extends Fragment implements SwipeRefreshLayout.OnRefre
@Override
public void onCommentsClick(final Media feedModel) {
final NavDirections commentsAction = FeedFragmentDirections.actionGlobalCommentsViewerFragment(
feedModel.getCode(),
feedModel.getPk(),
feedModel.getUser().getPk()
);
NavHostFragment.findNavController(FeedFragment.this).navigate(commentsAction);
try {
final NavDirections commentsAction = FeedFragmentDirections.actionGlobalCommentsViewerFragment(
feedModel.getCode(),
feedModel.getPk(),
feedModel.getUser().getPk()
);
NavHostFragment.findNavController(FeedFragment.this).navigate(commentsAction);
} catch (Exception e) {
Log.e(TAG, "onCommentsClick: ", e);
}
}
@Override

View File

@ -188,7 +188,7 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
@Override
public void onCommentsClick(final Media feedModel) {
final NavDirections commentsAction = FeedFragmentDirections.actionGlobalCommentsViewerFragment(
final NavDirections commentsAction = ProfileFragmentDirections.actionGlobalCommentsViewerFragment(
feedModel.getCode(),
feedModel.getPk(),
feedModel.getUser().getPk()
@ -991,7 +991,6 @@ public class ProfileFragment extends Fragment implements SwipeRefreshLayout.OnRe
if (chainingMenuItem != null) {
chainingMenuItem.setVisible(true);
}
return;
}
}

View File

@ -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 +
'}';
}
}

View File

@ -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 : "\"") + '}';
// }
// }
}

View File

@ -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 +
'}';
}
}

View File

@ -347,6 +347,18 @@ public final class Utils {
);
}
public static void showKeyboard(@NonNull final View view) {
final Context context = view.getContext();
if (context == null) return;
final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm == null) return;
view.requestFocus();
final boolean shown = imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
if (!shown) {
Log.e(TAG, "showKeyboard: System did not display the keyboard");
}
}
public static void hideKeyboard(final View view) {
if (view == null) return;
final Context context = view.getContext();

View File

@ -4,6 +4,7 @@ import android.app.Application;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
@ -36,6 +37,7 @@ public class AppStateViewModel extends AndroidViewModel {
fetchProfileDetails();
}
@Nullable
public User getCurrentUser() {
return currentUser.getValue();
}

View File

@ -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;
}
}

View File

@ -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()));
}
}

View File

@ -4,6 +4,8 @@ import android.util.Log;
import androidx.annotation.NonNull;
import com.google.common.collect.ImmutableMap;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -290,6 +292,20 @@ public class GraphQLService extends BaseService {
});
}
public Call<String> fetchComments(final String shortCodeOrCommentId,
final boolean root,
final String cursor) {
final Map<String, String> queryMap = new HashMap<>();
queryMap.put("query_hash", root ? "bc3296d1ce80a24b1b6e40b1e72903f5" : "51fdd02b67508306ad4484ff574a0b62");
final Map<String, Object> variables = ImmutableMap.of(
root ? "shortcode" : "comment_id", shortCodeOrCommentId,
"first", 50,
"after", cursor == null ? "" : cursor
);
queryMap.put("variables", new JSONObject(variables).toString());
return repository.fetch(queryMap);
}
public void fetchUser(final String username,
final ServiceCallback<User> callback) {
final Call<String> request = repository.getUser(username);
@ -305,7 +321,7 @@ public class GraphQLService extends BaseService {
try {
final JSONObject body = new JSONObject(rawBody);
final JSONObject userJson = body.getJSONObject("graphql")
.getJSONObject(Constants.EXTRAS_USER);
.getJSONObject(Constants.EXTRAS_USER);
boolean isPrivate = userJson.getBoolean("is_private");
final long id = userJson.optLong(Constants.EXTRAS_ID, 0);

View File

@ -1,12 +1,12 @@
package awais.instagrabber.webservices;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import org.json.JSONException;
import org.json.JSONObject;
@ -18,6 +18,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import awais.instagrabber.models.Comment;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.MediaRepository;
import awais.instagrabber.repositories.requests.UploadFinishOptions;
@ -27,6 +28,7 @@ import awais.instagrabber.repositories.responses.MediaInfoResponse;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.DateUtils;
import awais.instagrabber.utils.MediaUploadHelper;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import retrofit2.Call;
import retrofit2.Callback;
@ -170,7 +172,7 @@ public class MediaService extends BaseService {
public void comment(@NonNull final String mediaId,
@NonNull final String comment,
final String replyToCommentId,
@NonNull final ServiceCallback<Boolean> callback) {
@NonNull final ServiceCallback<Comment> callback) {
final String module = "self_comments_v2";
final Map<String, Object> form = new HashMap<>();
// form.put("user_breadcrumb", userBreadcrumb(comment.length()));
@ -191,15 +193,33 @@ public class MediaService extends BaseService {
final String body = response.body();
if (body == null) {
Log.e(TAG, "Error occurred while creating comment");
callback.onSuccess(false);
callback.onSuccess(null);
return;
}
try {
final JSONObject jsonObject = new JSONObject(body);
final String status = jsonObject.optString("status");
callback.onSuccess(status.equals("ok"));
} catch (JSONException e) {
// Log.e(TAG, "Error parsing body", e);
// final String status = jsonObject.optString("status");
final JSONObject commentJsonObject = jsonObject.optJSONObject("comment");
Comment comment = null;
if (commentJsonObject != null) {
final JSONObject userJsonObject = commentJsonObject.optJSONObject("user");
if (userJsonObject != null) {
final Gson gson = new Gson();
final User user = gson.fromJson(userJsonObject.toString(), User.class);
comment = new Comment(
commentJsonObject.optString("pk"),
commentJsonObject.optString("text"),
commentJsonObject.optLong("created_at"),
0,
false,
user,
0,
!TextUtils.isEmpty(replyToCommentId)
);
}
}
callback.onSuccess(comment);
} catch (Exception e) {
callback.onFailure(e);
}
}
@ -221,7 +241,7 @@ public class MediaService extends BaseService {
final List<String> commentIds,
@NonNull final ServiceCallback<Boolean> callback) {
final Map<String, Object> form = new HashMap<>();
form.put("comment_ids_to_delete", TextUtils.join(",", commentIds));
form.put("comment_ids_to_delete", android.text.TextUtils.join(",", commentIds));
form.put("_csrftoken", csrfToken);
form.put("_uid", userId);
form.put("_uuid", deviceUuid);

View File

@ -12,7 +12,7 @@ import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
class LoggingInterceptor implements Interceptor {
public class LoggingInterceptor implements Interceptor {
private static final String TAG = "LoggingInterceptor";
@NonNull
@ -30,7 +30,11 @@ class LoggingInterceptor implements Interceptor {
String content = "";
if (body != null) {
contentType = body.contentType();
content = body.string();
try {
content = body.string();
} catch (Exception e) {
Log.e(TAG, "intercept: ", e);
}
Log.d(TAG, content);
}
final ResponseBody wrappedBody = ResponseBody.create(contentType, content);

View File

@ -1,10 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<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"/>
</vector>
android:viewportHeight="24">
<path
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>

View File

@ -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>

View File

@ -1,10 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<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"/>
</vector>
android:viewportHeight="24">
<path
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>

View File

@ -1,49 +1,74 @@
<?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:tools="http://schemas.android.com/tools"
android:layout_width="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
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_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
android:id="@+id/rvComments"
android:id="@+id/comments"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
tools:listitem="@layout/item_comment" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<!--app:startIconDrawable="@drawable/ic_close_24"-->
<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_margin="4dp"
android:hint="@string/comment_hint"
android:visibility="gone"
app:counterEnabled="true"
app:counterEnabled="false"
app:counterMaxLength="2200"
app:endIconDrawable="@drawable/ic_round_send_24"
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">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/commentText"
android:id="@+id/comment_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="no"
android:inputType="textMultiLine"
android:maxLength="2200"
android:maxLines="10"
android:scrollHorizontally="false"
tools:text="test" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat 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:layout_width="match_parent"
android:layout_height="match_parent"
@ -16,6 +15,7 @@
android:id="@+id/rvLikes"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
android:clipToPadding="false"
tools:listitem="@layout/item_follow" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.appcompat.widget.LinearLayoutCompat>

View File

@ -5,125 +5,135 @@
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:selectableItemBackground"
android:clickable="true"
android:descendantFocusability="afterDescendants"
android:focusable="true"
android:orientation="horizontal"
android:padding="8dp">
android:foreground="?selectableItemBackground"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="4dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/ivProfilePic"
android:layout_width="@dimen/simple_item_picture_size"
android:layout_height="@dimen/simple_item_picture_size"
android:padding="4dp"
app:actualImageScaleType="centerCrop"
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="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:paddingStart="4dp"
android:paddingTop="2dp"
android:paddingEnd="4dp"
android:paddingBottom="2dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
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"
<androidx.constraintlayout.widget.Guideline
android:id="@+id/profile_pic_guideline"
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" />
android:orientation="vertical"
app:layout_constraintGuide_begin="56dp" />
<awais.instagrabber.customviews.RamboTextViewV2
android:id="@+id/tvComment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:autoLink="web|email"
android:ellipsize="end"
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_constraintHorizontal_bias="0.0"
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"
<awais.instagrabber.customviews.ProfilePicView
android:id="@+id/profile_pic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
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" />
android:layout_marginEnd="16dp"
app:actualImageScaleType="centerCrop"
app:layout_constraintEnd_toStartOf="@id/profile_pic_guideline"
app:layout_constraintHorizontal_bias="1"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintTop_toTopOf="parent"
app:size="small" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tvDate"
<awais.instagrabber.customviews.UsernameTextView
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:gravity="end"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:textStyle="italic"
app:layout_constraintBaseline_toBaselineOf="@id/tvLikes"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textColor="?android:textColorPrimary"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/date"
app:layout_constraintEnd_toStartOf="@id/options"
app:layout_constraintStart_toEndOf="@id/profile_pic_guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="username username username" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/options"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="?selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="4dp"
android:scaleType="fitCenter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_more_vert_24" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:alpha="0.8"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
app:layout_constraintBottom_toTopOf="@id/comment"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/username"
app:layout_constraintTop_toBottomOf="@id/username"
tools:text="long date..................." />
<awais.instagrabber.customviews.RamboTextViewV2
android:id="@+id/comment"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
app:layout_constraintBottom_toTopOf="@id/comment_barrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/username"
app:layout_constraintTop_toBottomOf="@id/date"
tools:text="comment comment comment comment comment comment comment comment comment comment comment comment" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/comment_barrier"
android:layout_width="0dp"
android:layout_height="0dp"
app:barrierDirection="bottom" />
<awais.instagrabber.customviews.TextViewDrawableSize
android:id="@+id/likes"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_marginEnd="16dp"
android:background="?selectableItemBackgroundBorderless"
android:clickable="true"
android:drawablePadding="4dp"
android:focusable="true"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
app:compoundDrawableHeight="16dp"
app:compoundDrawableWidth="16dp"
app:drawableTint="@color/red_800"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/replies"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@id/comment"
app:layout_constraintTop_toBottomOf="@id/comment_barrier"
tools:text="99999" />
<awais.instagrabber.customviews.TextViewDrawableSize
android:id="@+id/replies"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:background="?selectableItemBackgroundBorderless"
android:clickable="true"
android:drawablePadding="4dp"
android:focusable="true"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
app:compoundDrawableHeight="16dp"
app:compoundDrawableWidth="16dp"
app:drawableStartCompat="@drawable/ic_round_mode_comment_24"
app:drawableTint="@color/grey_500"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/tvLikes"
app:layout_constraintTop_toBottomOf="@id/tvComment"
tools:text="long date................................" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!--<androidx.recyclerview.widget.RecyclerView-->
<!-- android:id="@+id/rvChildComments"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_marginStart="40dp"-->
<!-- android:layout_marginLeft="40dp"-->
<!-- app:layoutManager="LinearLayoutManager"-->
<!-- tools:itemCount="5"-->
<!-- tools:listitem="@layout/item_comment_small" />-->
<!--<View-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="1dp"-->
<!-- android:layout_gravity="bottom"-->
<!-- android:layout_marginStart="4dp"-->
<!-- android:layout_marginEnd="4dp"-->
<!-- android:layout_marginBottom="4dp"-->
<!-- android:background="#32888888" />-->
app:layout_constraintStart_toEndOf="@id/likes"
app:layout_constraintTop_toBottomOf="@id/comment_barrier"
tools:text="9999" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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>

View File

@ -1,51 +1,42 @@
<?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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
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" />
<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" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginStart="66dp"
android:layout_marginEnd="26dp"
android:orientation="vertical">
<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
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" />
<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>
<androidx.appcompat.widget.AppCompatTextView
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>

View File

@ -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>

View File

@ -29,7 +29,7 @@
<dialog
android:id="@+id/commentsViewerFragment"
android:name="awais.instagrabber.fragments.CommentsViewerFragment"
android:name="awais.instagrabber.fragments.comments.CommentsViewerFragment"
android:label="Comments"
tools:layout="@layout/fragment_comments">
<argument

View File

@ -6,6 +6,7 @@
<enum name="small" value="1" />
<enum name="regular" value="2" />
<enum name="large" value="3" />
<enum name="smaller" value="4" />
</attr>
</declare-styleable>
<declare-styleable name="ChatMessageLayout">
@ -22,6 +23,7 @@
<attr name="toolbarColor" format="reference" />
<attr name="searchInputStyle" format="reference" />
<attr name="parentCommentHighlightColor" format="reference" />
<declare-styleable name="RecordView">
<attr name="slide_to_cancel_text" format="string" />
@ -37,4 +39,9 @@
<attr name="waveformBackgroundColor" format="reference" />
<attr name="waveformProgressColor" format="reference" />
</declare-styleable>
<declare-styleable name="TextViewDrawableSize">
<attr name="compoundDrawableWidth" format="dimension" />
<attr name="compoundDrawableHeight" format="dimension" />
</declare-styleable>
</resources>

View File

@ -25,8 +25,9 @@
<color name="dm_profile_button_color">#efefef</color>
<color name="comment_selected">#888888</color>
<color name="comment_liked">#40FF69B4</color>
<color name="parent_comment_dark_materialdark">#FF082654</color>
<color name="parent_comment_light_white">@color/grey_300</color>
<!--<color name="comment_liked">#40FF69B4</color>-->
<color name="white">#FFFFFF</color>
<color name="white_a50">#80FFFFFF</color>
@ -63,6 +64,10 @@
<color name="blue_A400">#2979FF</color>
<color name="blue_A700">#2962FF</color>
<color name="light_blue_900">#01579b</color>
<color name="indigo_900">#1A237E</color>
<color name="brown_50">#EFEBE9</color>
<color name="brown_100">#D7CCC8</color>
<color name="brown_200">#BCAAA4</color>

View File

@ -9,6 +9,7 @@
<dimen name="image_size_40">40dp</dimen>
<dimen name="profile_pic_size_tiny">24dp</dimen>
<dimen name="profile_pic_size_smaller">32dp</dimen>
<dimen name="profile_pic_size_small">40dp</dimen>
<dimen name="profile_pic_size_regular">48dp</dimen>
<dimen name="profile_pic_size_large">90dp</dimen>

View File

@ -214,7 +214,7 @@
<string name="comment_viewer_like_comment">Like comment</string>
<string name="comment_viewer_unlike_comment">Unlike comment</string>
<string name="comment_viewer_translate_comment">Translate comment</string>
<string name="comment_viewer_delete_comment">Delete comment</string>
<!--<string name="comment_viewer_delete_comment">Delete comment</string>-->
<string name="comment_send_empty_comment">No empty comments!</string>
<string name="comment_view_mention_user_search">Do you want to search the username?</string>
<string name="comment_view_mention_hash_search">Do you want to search the hashtag?</string>
@ -381,6 +381,10 @@
<item quantity="one">%d like</item>
<item quantity="other">%d likes</item>
</plurals>
<plurals name="replies_count">
<item quantity="one">%d reply</item>
<item quantity="other">%d replies</item>
</plurals>
<plurals name="comments_count">
<item quantity="one">%d comment</item>
<item quantity="other">%d comments</item>

View File

@ -261,13 +261,13 @@
<item name="colorPrimary">@color/white</item>
</style>
<style name="Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dark.Black" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<item name="materialThemeOverlay">@style/ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox.Dark.Black</item>
<style name="Widget.MaterialComponents.TextInputLayout.FilledBox.Dark.Black" parent="Widget.MaterialComponents.TextInputLayout.FilledBox">
<item name="materialThemeOverlay">@style/ThemeOverlay.MaterialComponents.TextInputEditText.FilledBox.Dark.Black</item>
<item name="hintTextColor">@color/white</item>
<item name="boxStrokeColor">@color/white</item>
</style>
<style name="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox.Dark.Black" parent="ThemeOverlay.MaterialComponents.TextInputEditText.OutlinedBox">
<style name="ThemeOverlay.MaterialComponents.TextInputEditText.FilledBox.Dark.Black" parent="ThemeOverlay.MaterialComponents.TextInputEditText.FilledBox">
<item name="colorPrimary">@color/white</item>
</style>
</resources>

View File

@ -53,6 +53,7 @@
<item name="dmWaveformProgressColor">@color/blue_800</item>
<item name="dmInputTextColor">@color/black</item>
<item name="tabStyle">@style/Widget.MaterialComponents.TabLayout.Light.White</item>
<item name="parentCommentHighlightColor">@color/parent_comment_light_white</item>
</style>
<style name="AppTheme.Light.Barinsta" parent="AppTheme.Light">
@ -72,6 +73,7 @@
<item name="toolbarStyle">@style/Widget.MaterialComponents.Toolbar.Light.Barinsta</item>
<item name="dmInputTextColor">@color/black</item>
<item name="searchInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Light.Barinsta</item>
<item name="parentCommentHighlightColor">@color/parent_comment_light_white</item>
</style>
<style name="AppTheme.Light.Bibliogram" parent="AppTheme.Light">
@ -90,6 +92,7 @@
<item name="toolbarStyle">@style/Widget.MaterialComponents.Toolbar.Light.Barinsta</item>
<item name="dmInputTextColor">@color/black</item>
<item name="searchInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Light.Barinsta</item>
<item name="parentCommentHighlightColor">@color/parent_comment_light_white</item>
</style>
@ -146,7 +149,8 @@
<item name="dmOutgoingBgColor">@color/deep_purple_400</item>
<item name="dmDateHeaderBgColor">@color/deep_purple_600</item>
<item name="tabStyle">@style/Widget.MaterialComponents.TabLayout.Dark.Black</item>
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dark.Black</item>
<item name="textInputStyle">@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dark.Black</item>
<item name="parentCommentHighlightColor">@color/parent_comment_dark_materialdark</item>
</style>
<style name="AppTheme.Dark.MaterialDark" parent="AppTheme.Dark">
@ -169,6 +173,7 @@
<item name="dmIncomingBgColor">@color/grey_600</item>
<item name="dmOutgoingBgColor">@color/deep_purple_400</item>
<item name="dmDateHeaderBgColor">@color/deep_purple_600</item>
<item name="parentCommentHighlightColor">@color/parent_comment_dark_materialdark</item>
</style>
<style name="AppTheme.Launcher" parent="AppTheme.Dark">