Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save followthemoney1/c4ce177839cd256a31e302e9695116cf to your computer and use it in GitHub Desktop.
Save followthemoney1/c4ce177839cd256a31e302e9695116cf to your computer and use it in GitHub Desktop.
plaid many expand adapter
class DesignerNewsCommentsAdapter
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HEADER = 0;
private static final int TYPE_NO_COMMENTS = 1;
private static final int TYPE_COMMENT = 2;
private static final int TYPE_COMMENT_REPLY = 3;
private static final int TYPE_FOOTER = 4;
private View header;
private List<Comment> comments;
private View footer;
private int expandedCommentPosition = RecyclerView.NO_POSITION;
private boolean replyToCommentFocused = false;
DesignerNewsCommentsAdapter(@NonNull View header,
@NonNull List<Comment> comments,
@NonNull View footer) {
this.header = header;
this.comments = comments;
this.footer = footer;
}
public void updateList(List<Comment> comments) {
this.comments = comments;
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
if (position == 0) return TYPE_HEADER;
if (isCommentReplyExpanded() && position == expandedCommentPosition + 1) {
return TYPE_COMMENT_REPLY;
}
int footerPosition = hasComments() ? 1 + comments.size() // header + comments
: 2; // header + no comments view
if (isCommentReplyExpanded()) footerPosition++;
if (position == footerPosition) return TYPE_FOOTER;
return hasComments() ? TYPE_COMMENT : TYPE_NO_COMMENTS;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case TYPE_HEADER:
return new HeaderHolder(header);
case TYPE_COMMENT:
return createCommentHolder(parent);
case TYPE_COMMENT_REPLY:
return createCommentReplyHolder(parent);
case TYPE_NO_COMMENTS:
return new NoCommentsHolder(
getLayoutInflater().inflate(
R.layout.designer_news_no_comments, parent, false));
case TYPE_FOOTER:
return new FooterHolder(footer);
}
return null;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (getItemViewType(position)) {
case TYPE_COMMENT:
bindComment((CommentViewHolder) holder, null);
break;
case TYPE_COMMENT_REPLY:
((CommentReplyViewHolder) holder).bindCommentReply(
getComment(holder.getAdapterPosition() - 1));
break;
} // nothing to bind for header / no comment / footer views
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder,
int position,
List<Object> partialChangePayloads) {
switch (getItemViewType(position)) {
case TYPE_COMMENT:
bindComment((CommentViewHolder) holder, partialChangePayloads);
break;
default:
onBindViewHolder(holder, position);
}
}
@Override
public int getItemCount() {
int itemCount = 2; // header + footer
if (hasComments()) {
itemCount += comments.size();
} else {
itemCount++; // no comments view
}
if (isCommentReplyExpanded()) itemCount++;
return itemCount;
}
public void addComment(Comment newComment) {
if (!hasComments()) {
notifyItemRemoved(1); // remove the no comments view
}
comments.add(newComment);
notifyItemInserted(commentIndexToAdapterPosition(comments.size() - 1));
}
/**
* Add a new comment and return the adapter position that it was inserted at.
*/
public int addCommentReply(Comment newComment, int inReplyToAdapterPosition) {
// when replying to a comment, we want to insert it after any existing replies
// i.e. after any following comments with the same or greater depth
int commentIndex = adapterPositionToCommentIndex(inReplyToAdapterPosition);
do {
commentIndex++;
} while (commentIndex < comments.size() &&
comments.get(commentIndex).getDepth() >= newComment.getDepth());
comments.add(commentIndex, newComment);
int adapterPosition = commentIndexToAdapterPosition(commentIndex);
notifyItemInserted(adapterPosition);
return adapterPosition;
}
public boolean isReplyToCommentFocused() {
return replyToCommentFocused;
}
private boolean hasComments() {
return !comments.isEmpty();
}
private boolean isCommentReplyExpanded() {
return expandedCommentPosition != RecyclerView.NO_POSITION;
}
private Comment getComment(int adapterPosition) {
return comments.get(adapterPositionToCommentIndex(adapterPosition));
}
private int adapterPositionToCommentIndex(int adapterPosition) {
int index = adapterPosition - 1; // less header
if (isCommentReplyExpanded()
&& adapterPosition > expandedCommentPosition) {
index--;
}
return index;
}
private int commentIndexToAdapterPosition(int index) {
int adapterPosition = index + 1; // header
if (isCommentReplyExpanded()) {
int expandedCommentIndex = adapterPositionToCommentIndex(expandedCommentPosition);
if (index > expandedCommentIndex) adapterPosition++;
}
return adapterPosition;
}
@NonNull
private CommentViewHolder createCommentHolder(ViewGroup parent) {
final CommentViewHolder holder = new CommentViewHolder(
getLayoutInflater().inflate(R.layout.designer_news_comment, parent, false),
threadWidth, threadGap);
holder.itemView.setOnClickListener(v -> {
final boolean collapsingSelf =
expandedCommentPosition == holder.getAdapterPosition();
collapseExpandedComment();
if (collapsingSelf) return;
// show reply below this
expandedCommentPosition = holder.getAdapterPosition();
notifyItemInserted(expandedCommentPosition + 1);
notifyItemChanged(expandedCommentPosition,
CommentAnimator.EXPAND_COMMENT);
});
return holder;
}
private void collapseExpandedComment() {
if (!isCommentReplyExpanded()) return;
notifyItemChanged(expandedCommentPosition,
CommentAnimator.COLLAPSE_COMMENT);
notifyItemRemoved(expandedCommentPosition + 1);
replyToCommentFocused = false;
expandedCommentPosition = RecyclerView.NO_POSITION;
updateFabVisibility();
}
private void bindComment(final CommentViewHolder holder, List<Object> partialChanges) {
// Check if this is a partial update for expanding/collapsing a comment. If it is we
// can do a partial bind as the bound data has not changed.
if (partialChanges == null || partialChanges.isEmpty() ||
!(partialChanges.contains(CommentAnimator.COLLAPSE_COMMENT)
|| partialChanges.contains(
CommentAnimator.EXPAND_COMMENT))) {
final Comment comment = getComment(holder.getAdapterPosition());
ColorStateList linksColor = ContextCompat.getColorStateList(getApplicationContext(),
R.color.designer_news_links);
int highlightColor = ContextCompat.getColor(getApplicationContext(),
io.plaidapp.R.color.designer_news_link_highlight);
CharSequence commentText = HtmlUtils.parseMarkdownAndPlainLinks(
comment.getBody(),
markdown,
linksColor,
highlightColor,
(src, loadingSpan) -> GlideApp.with(StoryActivity.this)
.asBitmap()
.load(src)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(new ImageSpanTarget(holder.getComment(), loadingSpan)));
String author = comment.getUserDisplayName() != null ? comment.getUserDisplayName().toLowerCase() : "";
boolean isOriginalPoster = isOP(comment.getUserId());
String timeAgo = DateUtils.getRelativeTimeSpanString(
comment.getCreatedAt().getTime(),
System.currentTimeMillis(),
DateUtils.SECOND_IN_MILLIS)
.toString().toLowerCase();
CommentUiModel commentUiModel = new CommentUiModel(
commentText,
timeAgo,
comment.getDepth(),
author,
isOriginalPoster
);
holder.bind(commentUiModel);
}
// set/clear expanded comment state
holder.itemView.setActivated(holder.getAdapterPosition() == expandedCommentPosition);
holder.setExpanded(holder.getAdapterPosition() == expandedCommentPosition);
}
private void replyToComment(Long commentId, String reply) {
// TODO move the result handling in the VM
viewModel.commentReplyRequested(reply, commentId, result -> {
if (result instanceof Result.Error) {
Toast.makeText(getApplicationContext(),
"Failed to post comment :(", Toast.LENGTH_SHORT).show();
}
return Unit.INSTANCE;
});
}
private void handleCommentVotesClick(CommentReplyViewHolder holder,
boolean isUserLoggedIn,
Comment comment) {
if (isUserLoggedIn) {
if (!holder.getCommentVotes().isActivated()) {
viewModel.commentUpvoteRequested(story.getId(),
result -> {
if (result instanceof Result.Success) {
comment.setUpvoted(true);
// TODO fix this
// comment.vote_count++;
holder.getCommentVotes().setText(String.valueOf(comment.getUpvotesCount()));
holder.getCommentVotes().setActivated(true);
} else {
Toast.makeText(StoryActivity.this, "Unable to upvote comment",
Toast.LENGTH_LONG)
.show();
}
return Unit.INSTANCE;
});
} else {
comment.setUpvoted(false);
// TODO fix this
// comment.setVoteCount(comment.getVoteCount() - 1);
holder.getCommentVotes().setText(String.valueOf(comment.getUpvotesCount()));
holder.getCommentVotes().setActivated(false);
// TODO actually delete upvote - florina: why?
}
} else {
needsLogin(holder.getCommentVotes(), 0);
}
holder.getCommentReply().clearFocus();
}
@NonNull
private CommentReplyViewHolder createCommentReplyHolder(ViewGroup parent) {
final CommentReplyViewHolder holder = new CommentReplyViewHolder(getLayoutInflater()
.inflate(R.layout.designer_news_comment_actions, parent, false));
holder.getCommentVotes().setOnClickListener(v -> {
Comment comment = getComment(holder.getAdapterPosition());
handleCommentVotesClick(holder, loginRepository.isLoggedIn(), comment);
});
holder.getPostReply().setOnClickListener(v -> {
if (loginRepository.isLoggedIn()) {
String reply = holder.getCommentReply().getText().toString();
if (reply.isEmpty()) return;
final int inReplyToCommentPosition = holder.getAdapterPosition() - 1;
final Comment replyingTo = getComment(inReplyToCommentPosition);
collapseExpandedComment();
// insert a locally created comment before actually
// hitting the API for immediate response
int replyDepth = replyingTo.getDepth() + 1;
LoggedInUser user = loginRepository.getUser();
String commentBody = holder.getCommentReply().getText().toString();
final int newReplyPosition = commentsAdapter.addCommentReply(
new Comment(
0,
replyingTo.getId(),
commentBody,
new Date(),
replyDepth,
0,
user.getId(),
user.getDisplayName(),
user.getPortraitUrl(),
false
),
inReplyToCommentPosition);
replyToComment(replyingTo.getId(), reply);
holder.getCommentReply().getText().clear();
ImeUtils.hideIme(holder.getCommentReply());
commentsList.scrollToPosition(newReplyPosition);
} else {
needsLogin(holder.getPostReply(), 0);
}
holder.getCommentReply().clearFocus();
});
holder.getCommentReply().setOnFocusChangeListener((v, hasFocus) -> {
replyToCommentFocused = hasFocus;
if (hasFocus) {
holder.createCommentReplyFocusAnimator().start();
} else {
holder.createCommentReplyFocusLossAnimator().start();
}
updateFabVisibility();
holder.getPostReply().setActivated(hasFocus);
});
return holder;
}
}
/* package */ static class HeaderHolder extends RecyclerView.ViewHolder {
public HeaderHolder(View itemView) {
super(itemView);
}
}
/* package */ static class NoCommentsHolder extends RecyclerView.ViewHolder {
public NoCommentsHolder(View itemView) {
super(itemView);
}
}
/* package */ static class FooterHolder extends RecyclerView.ViewHolder {
public FooterHolder(View itemView) {
super(itemView);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment