| package org.wordpress.android.ui.reader.adapters; |
| |
| import android.content.Context; |
| import android.graphics.Color; |
| import android.os.AsyncTask; |
| import android.support.v4.content.ContextCompat; |
| import android.support.v7.widget.RecyclerView; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.ImageView; |
| import android.widget.ProgressBar; |
| import android.widget.RelativeLayout; |
| import android.widget.TextView; |
| |
| import org.wordpress.android.R; |
| import org.wordpress.android.WordPress; |
| import org.wordpress.android.datasets.ReaderCommentTable; |
| import org.wordpress.android.datasets.ReaderPostTable; |
| import org.wordpress.android.models.ReaderComment; |
| import org.wordpress.android.models.ReaderCommentList; |
| import org.wordpress.android.models.ReaderPost; |
| import org.wordpress.android.ui.comments.CommentUtils; |
| import org.wordpress.android.ui.reader.ReaderActivityLauncher; |
| import org.wordpress.android.ui.reader.ReaderAnim; |
| import org.wordpress.android.ui.reader.ReaderInterfaces; |
| import org.wordpress.android.ui.reader.actions.ReaderActions; |
| import org.wordpress.android.ui.reader.actions.ReaderCommentActions; |
| import org.wordpress.android.ui.reader.utils.ReaderLinkMovementMethod; |
| import org.wordpress.android.ui.reader.utils.ReaderUtils; |
| import org.wordpress.android.ui.reader.views.ReaderCommentsPostHeaderView; |
| import org.wordpress.android.ui.reader.views.ReaderIconCountView; |
| import org.wordpress.android.util.AppLog; |
| import org.wordpress.android.util.AppLog.T; |
| import org.wordpress.android.util.DateTimeUtils; |
| import org.wordpress.android.util.DisplayUtils; |
| import org.wordpress.android.util.GravatarUtils; |
| import org.wordpress.android.util.NetworkUtils; |
| import org.wordpress.android.util.ToastUtils; |
| import org.wordpress.android.widgets.WPNetworkImageView; |
| |
| public class ReaderCommentAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { |
| private ReaderPost mPost; |
| private boolean mMoreCommentsExist; |
| |
| private static final int MAX_INDENT_LEVEL = 2; |
| private final int mIndentPerLevel; |
| private final int mAvatarSz; |
| private final int mContentWidth; |
| |
| private long mHighlightCommentId = 0; |
| private boolean mShowProgressForHighlightedComment = false; |
| private final boolean mIsPrivatePost; |
| private final boolean mIsLoggedOutReader; |
| private boolean mIsHeaderClickEnabled; |
| |
| private final int mColorAuthor; |
| private final int mColorNotAuthor; |
| private final int mColorHighlight; |
| |
| private static final int VIEW_TYPE_HEADER = 1; |
| private static final int VIEW_TYPE_COMMENT = 2; |
| |
| private static final long ID_HEADER = -1L; |
| |
| private static final int NUM_HEADERS = 1; |
| |
| public interface RequestReplyListener { |
| void onRequestReply(long commentId); |
| } |
| |
| private ReaderCommentList mComments = new ReaderCommentList(); |
| private RequestReplyListener mReplyListener; |
| private ReaderInterfaces.DataLoadedListener mDataLoadedListener; |
| private ReaderActions.DataRequestedListener mDataRequestedListener; |
| |
| class CommentHolder extends RecyclerView.ViewHolder { |
| private final ViewGroup container; |
| private final TextView txtAuthor; |
| private final TextView txtText; |
| private final TextView txtDate; |
| |
| private final WPNetworkImageView imgAvatar; |
| private final View spacerIndent; |
| private final ProgressBar progress; |
| |
| private final TextView txtReply; |
| private final ImageView imgReply; |
| |
| private final ReaderIconCountView countLikes; |
| |
| public CommentHolder(View view) { |
| super(view); |
| |
| container = (ViewGroup) view.findViewById(R.id.layout_container); |
| |
| txtAuthor = (TextView) view.findViewById(R.id.text_comment_author); |
| txtText = (TextView) view.findViewById(R.id.text_comment_text); |
| txtDate = (TextView) view.findViewById(R.id.text_comment_date); |
| |
| txtReply = (TextView) view.findViewById(R.id.text_comment_reply); |
| imgReply = (ImageView) view.findViewById(R.id.image_comment_reply); |
| |
| imgAvatar = (WPNetworkImageView) view.findViewById(R.id.image_comment_avatar); |
| spacerIndent = view.findViewById(R.id.spacer_comment_indent); |
| progress = (ProgressBar) view.findViewById(R.id.progress_comment); |
| |
| countLikes = (ReaderIconCountView) view.findViewById(R.id.count_likes); |
| |
| txtText.setLinksClickable(true); |
| txtText.setMovementMethod(ReaderLinkMovementMethod.getInstance(mIsPrivatePost)); |
| } |
| } |
| |
| class PostHeaderHolder extends RecyclerView.ViewHolder { |
| private final ReaderCommentsPostHeaderView mHeaderView; |
| |
| public PostHeaderHolder(View view) { |
| super(view); |
| mHeaderView = (ReaderCommentsPostHeaderView) view; |
| } |
| } |
| |
| public ReaderCommentAdapter(Context context, ReaderPost post) { |
| mPost = post; |
| mIsPrivatePost = (post != null && post.isPrivate); |
| mIsLoggedOutReader = ReaderUtils.isLoggedOutReader(); |
| |
| mIndentPerLevel = context.getResources().getDimensionPixelSize(R.dimen.reader_comment_indent_per_level); |
| mAvatarSz = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_extra_small); |
| |
| // calculate the max width of comment content |
| int displayWidth = DisplayUtils.getDisplayPixelWidth(context); |
| int cardMargin = context.getResources().getDimensionPixelSize(R.dimen.reader_card_margin); |
| int contentPadding = context.getResources().getDimensionPixelSize(R.dimen.reader_card_content_padding); |
| int mediumMargin = context.getResources().getDimensionPixelSize(R.dimen.margin_medium); |
| mContentWidth = displayWidth - (cardMargin * 2) - (contentPadding * 2) - (mediumMargin * 2); |
| |
| mColorAuthor = ContextCompat.getColor(context, R.color.blue_medium); |
| mColorNotAuthor = ContextCompat.getColor(context, R.color.grey_dark); |
| mColorHighlight = ContextCompat.getColor(context, R.color.grey_lighten_30); |
| |
| setHasStableIds(true); |
| } |
| |
| public void setReplyListener(RequestReplyListener replyListener) { |
| mReplyListener = replyListener; |
| } |
| |
| public void setDataLoadedListener(ReaderInterfaces.DataLoadedListener dataLoadedListener) { |
| mDataLoadedListener = dataLoadedListener; |
| } |
| |
| public void setDataRequestedListener(ReaderActions.DataRequestedListener dataRequestedListener) { |
| mDataRequestedListener = dataRequestedListener; |
| } |
| |
| public void enableHeaderClicks() { |
| mIsHeaderClickEnabled = true; |
| } |
| |
| @Override |
| public int getItemViewType(int position) { |
| return position == 0 ? VIEW_TYPE_HEADER : VIEW_TYPE_COMMENT; |
| } |
| |
| public void refreshComments() { |
| if (mIsTaskRunning) { |
| AppLog.w(T.READER, "reader comment adapter > Load comments task already running"); |
| } |
| new LoadCommentsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| } |
| |
| @Override |
| public int getItemCount() { |
| return mComments.size() + NUM_HEADERS; |
| } |
| |
| public boolean isEmpty() { |
| return mComments.size() == 0; |
| } |
| |
| @Override |
| public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
| switch (viewType) { |
| case VIEW_TYPE_HEADER: |
| View headerView = new ReaderCommentsPostHeaderView(parent.getContext()); |
| headerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); |
| return new PostHeaderHolder(headerView); |
| default: |
| View commentView = LayoutInflater.from(parent.getContext()).inflate(R.layout.reader_listitem_comment, parent, false); |
| return new CommentHolder(commentView); |
| } |
| } |
| |
| @Override |
| public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { |
| if (holder instanceof PostHeaderHolder) { |
| PostHeaderHolder headerHolder = (PostHeaderHolder) holder; |
| headerHolder.mHeaderView.setPost(mPost); |
| if (mIsHeaderClickEnabled) { |
| headerHolder.mHeaderView.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| ReaderActivityLauncher.showReaderPostDetail(view.getContext(), mPost.blogId, mPost.postId); |
| } |
| }); |
| } |
| return; |
| } |
| |
| final ReaderComment comment = getItem(position); |
| if (comment == null) { |
| return; |
| } |
| |
| CommentHolder commentHolder = (CommentHolder) holder; |
| commentHolder.txtAuthor.setText(comment.getAuthorName()); |
| |
| java.util.Date dtPublished = DateTimeUtils.dateFromIso8601(comment.getPublished()); |
| commentHolder.txtDate.setText(DateTimeUtils.javaDateToTimeSpan(dtPublished, WordPress.getContext())); |
| |
| if (comment.hasAuthorAvatar()) { |
| String avatarUrl = GravatarUtils.fixGravatarUrl(comment.getAuthorAvatar(), mAvatarSz); |
| commentHolder.imgAvatar.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR); |
| } else { |
| commentHolder.imgAvatar.showDefaultGravatarImage(); |
| } |
| |
| // tapping avatar or author name opens blog preview |
| if (comment.hasAuthorBlogId()) { |
| View.OnClickListener authorListener = new View.OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| ReaderActivityLauncher.showReaderBlogPreview( |
| view.getContext(), |
| comment.authorBlogId |
| ); |
| } |
| }; |
| commentHolder.imgAvatar.setOnClickListener(authorListener); |
| commentHolder.txtAuthor.setOnClickListener(authorListener); |
| } else { |
| commentHolder.imgAvatar.setOnClickListener(null); |
| commentHolder.txtAuthor.setOnClickListener(null); |
| } |
| |
| // author name uses different color for comments from the post's author |
| if (comment.authorId == mPost.authorId) { |
| commentHolder.txtAuthor.setTextColor(mColorAuthor); |
| } else { |
| commentHolder.txtAuthor.setTextColor(mColorNotAuthor); |
| } |
| |
| // show indentation spacer for comments with parents and indent it based on comment level |
| int indentWidth; |
| if (comment.parentId != 0 && comment.level > 0) { |
| indentWidth = Math.min(MAX_INDENT_LEVEL, comment.level) * mIndentPerLevel; |
| RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) commentHolder.spacerIndent.getLayoutParams(); |
| params.width = indentWidth; |
| commentHolder.spacerIndent.setVisibility(View.VISIBLE); |
| } else { |
| indentWidth = 0; |
| commentHolder.spacerIndent.setVisibility(View.GONE); |
| } |
| |
| int maxImageWidth = mContentWidth - indentWidth; |
| CommentUtils.displayHtmlComment(commentHolder.txtText, comment.getText(), maxImageWidth); |
| |
| // different background for highlighted comment, with optional progress bar |
| if (mHighlightCommentId != 0 && mHighlightCommentId == comment.commentId) { |
| commentHolder.container.setBackgroundColor(mColorHighlight); |
| commentHolder.progress.setVisibility(mShowProgressForHighlightedComment ? View.VISIBLE : View.GONE); |
| } else { |
| commentHolder.container.setBackgroundColor(Color.WHITE); |
| commentHolder.progress.setVisibility(View.GONE); |
| } |
| |
| if (mIsLoggedOutReader) { |
| commentHolder.txtReply.setVisibility(View.GONE); |
| commentHolder.imgReply.setVisibility(View.GONE); |
| } else if (mReplyListener != null) { |
| // tapping reply icon tells activity to show reply box |
| View.OnClickListener replyClickListener = new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| mReplyListener.onRequestReply(comment.commentId); |
| } |
| }; |
| commentHolder.txtReply.setOnClickListener(replyClickListener); |
| commentHolder.imgReply.setOnClickListener(replyClickListener); |
| } |
| |
| showLikeStatus(commentHolder, position); |
| |
| // if we're nearing the end of the comments and we know more exist on the server, |
| // fire request to load more |
| if (mMoreCommentsExist && mDataRequestedListener != null && (position >= getItemCount() - NUM_HEADERS)) { |
| mDataRequestedListener.onRequestData(); |
| } |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| switch (getItemViewType(position)) { |
| case VIEW_TYPE_HEADER: |
| return ID_HEADER; |
| default: |
| ReaderComment comment = getItem(position); |
| return comment != null ? comment.commentId : 0; |
| } |
| } |
| |
| private ReaderComment getItem(int position) { |
| return position == 0 ? null : mComments.get(position - NUM_HEADERS); |
| } |
| |
| private void showLikeStatus(final CommentHolder holder, int position) { |
| ReaderComment comment = getItem(position); |
| if (comment == null) { |
| return; |
| } |
| |
| if (mPost.canLikePost()) { |
| holder.countLikes.setVisibility(View.VISIBLE); |
| holder.countLikes.setSelected(comment.isLikedByCurrentUser); |
| holder.countLikes.setCount(comment.numLikes); |
| |
| if (mIsLoggedOutReader) { |
| holder.countLikes.setEnabled(false); |
| } else { |
| holder.countLikes.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| int clickedPosition = holder.getAdapterPosition(); |
| toggleLike(v.getContext(), holder, clickedPosition); |
| } |
| }); |
| } |
| } else { |
| holder.countLikes.setVisibility(View.GONE); |
| holder.countLikes.setOnClickListener(null); |
| } |
| } |
| |
| private void toggleLike(Context context, CommentHolder holder, int position) { |
| if (!NetworkUtils.checkConnection(context)) { |
| return; |
| } |
| |
| ReaderComment comment = getItem(position); |
| if (comment == null) { |
| ToastUtils.showToast(context, R.string.reader_toast_err_generic); |
| return; |
| } |
| |
| boolean isAskingToLike = !comment.isLikedByCurrentUser; |
| ReaderAnim.animateLikeButton(holder.countLikes.getImageView(), isAskingToLike); |
| |
| if (!ReaderCommentActions.performLikeAction(comment, isAskingToLike)) { |
| ToastUtils.showToast(context, R.string.reader_toast_err_generic); |
| return; |
| } |
| |
| ReaderComment updatedComment = ReaderCommentTable.getComment(comment.blogId, comment.postId, comment.commentId); |
| if (updatedComment != null) { |
| mComments.set(position - NUM_HEADERS, updatedComment); |
| showLikeStatus(holder, position); |
| } |
| } |
| |
| /* |
| * called from post detail activity when user submits a comment |
| */ |
| public void addComment(ReaderComment comment) { |
| if (comment == null) { |
| return; |
| } |
| |
| // if the comment doesn't have a parent we can just add it to the list of existing |
| // comments - but if it does have a parent, we need to reload the list so that it |
| // appears under its parent and is correctly indented |
| if (comment.parentId == 0) { |
| mComments.add(comment); |
| notifyDataSetChanged(); |
| } else { |
| refreshComments(); |
| } |
| } |
| |
| /* |
| * called from post detail when submitted a comment fails - this removes the "fake" comment |
| * that was inserted while the API call was still being processed |
| */ |
| public void removeComment(long commentId) { |
| if (commentId == mHighlightCommentId) { |
| setHighlightCommentId(0, false); |
| } |
| |
| int index = mComments.indexOfCommentId(commentId); |
| if (index > -1) { |
| mComments.remove(index); |
| notifyDataSetChanged(); |
| } |
| } |
| |
| /* |
| * replace the comment that has the passed commentId with another comment |
| */ |
| public void replaceComment(long commentId, ReaderComment comment) { |
| int position = positionOfCommentId(commentId); |
| if (position > -1 && mComments.replaceComment(commentId, comment)) { |
| notifyItemChanged(position); |
| } |
| } |
| |
| /* |
| * sets the passed comment as highlighted with a different background color and an optional |
| * progress bar (used when posting new comments) - note that we don't call notifyDataSetChanged() |
| * here since in most cases it's unnecessary, so we leave it up to the caller to do that |
| */ |
| public void setHighlightCommentId(long commentId, boolean showProgress) { |
| mHighlightCommentId = commentId; |
| mShowProgressForHighlightedComment = showProgress; |
| } |
| |
| /* |
| * returns the position of the passed comment in the adapter, taking the header into account |
| */ |
| public int positionOfCommentId(long commentId) { |
| int index = mComments.indexOfCommentId(commentId); |
| return index == -1 ? -1 : index + NUM_HEADERS; |
| } |
| |
| /* |
| * AsyncTask to load comments for this post |
| */ |
| private boolean mIsTaskRunning = false; |
| |
| private class LoadCommentsTask extends AsyncTask<Void, Void, Boolean> { |
| private ReaderCommentList tmpComments; |
| private boolean tmpMoreCommentsExist; |
| |
| @Override |
| protected void onPreExecute() { |
| mIsTaskRunning = true; |
| } |
| |
| @Override |
| protected void onCancelled() { |
| mIsTaskRunning = false; |
| } |
| |
| @Override |
| protected Boolean doInBackground(Void... params) { |
| if (mPost == null) { |
| return false; |
| } |
| |
| // determine whether more comments can be downloaded by comparing the number of |
| // comments the post says it has with the number of comments actually stored |
| // locally for this post |
| int numServerComments = ReaderPostTable.getNumCommentsForPost(mPost); |
| int numLocalComments = ReaderCommentTable.getNumCommentsForPost(mPost); |
| tmpMoreCommentsExist = (numServerComments > numLocalComments); |
| |
| tmpComments = ReaderCommentTable.getCommentsForPost(mPost); |
| return !mComments.isSameList(tmpComments); |
| } |
| |
| @Override |
| protected void onPostExecute(Boolean result) { |
| mMoreCommentsExist = tmpMoreCommentsExist; |
| |
| if (result) { |
| // assign the comments with children sorted under their parents and indent levels applied |
| mComments = ReaderCommentList.getLevelList(tmpComments); |
| notifyDataSetChanged(); |
| } |
| if (mDataLoadedListener != null) { |
| mDataLoadedListener.onDataLoaded(isEmpty()); |
| } |
| mIsTaskRunning = false; |
| } |
| } |
| |
| /* |
| * Set a post to adapter and update relevant information in the post header |
| */ |
| public void setPost(ReaderPost post) { |
| if (post != null) { |
| mPost = post; |
| notifyItemChanged(0); //notify header to update itself |
| } |
| |
| } |
| } |