package org.wordpress.android.ui.comments; import android.content.Context; import android.os.AsyncTask; import android.support.v4.content.ContextCompat; import android.support.v7.widget.RecyclerView; import android.text.Html; import android.text.Spanned; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.fluxc.model.CommentModel; import org.wordpress.android.fluxc.model.CommentStatus; import org.wordpress.android.fluxc.model.SiteModel; import org.wordpress.android.fluxc.store.CommentStore; import org.wordpress.android.models.CommentList; import org.wordpress.android.util.AniUtils; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.DateTimeUtils; import org.wordpress.android.util.GravatarUtils; import org.wordpress.android.util.HtmlUtils; import org.wordpress.android.util.StringUtils; import org.wordpress.android.util.WPHtml; import org.wordpress.android.widgets.WPNetworkImageView; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.List; import javax.inject.Inject; public class CommentAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { interface OnDataLoadedListener { void onDataLoaded(boolean isEmpty); } interface OnLoadMoreListener { void onLoadMore(); } interface OnSelectedItemsChangeListener { void onSelectedItemsChanged(); } interface OnCommentPressedListener { void onCommentPressed(int position, View view); void onCommentLongPressed(int position, View view); } private final LayoutInflater mInflater; private final Context mContext; private final CommentList mComments = new CommentList(); private final HashSet<Integer> mSelectedPositions = new HashSet<>(); private final int mStatusColorSpam; private final int mStatusColorUnapproved; private final int mAvatarSz; private final String mStatusTextSpam; private final String mStatusTextUnapproved; private final int mSelectedColor; private final int mUnselectedColor; private OnDataLoadedListener mOnDataLoadedListener; private OnCommentPressedListener mOnCommentPressedListener; private OnLoadMoreListener mOnLoadMoreListener; private OnSelectedItemsChangeListener mOnSelectedChangeListener; private boolean mEnableSelection; private SiteModel mSite; @Inject CommentStore mCommentStore; class CommentHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { private final TextView txtTitle; private final TextView txtComment; private final TextView txtStatus; private final TextView txtDate; private final WPNetworkImageView imgAvatar; private final ImageView imgCheckmark; private final ViewGroup containerView; public CommentHolder(View view) { super(view); txtTitle = (TextView) view.findViewById(R.id.title); txtComment = (TextView) view.findViewById(R.id.comment); txtStatus = (TextView) view.findViewById(R.id.status); txtDate = (TextView) view.findViewById(R.id.text_date); imgCheckmark = (ImageView) view.findViewById(R.id.image_checkmark); imgAvatar = (WPNetworkImageView) view.findViewById(R.id.avatar); containerView = (ViewGroup) view.findViewById(R.id.layout_container); itemView.setOnClickListener(this); itemView.setOnLongClickListener(this); } @Override public void onClick(View v) { if (mOnCommentPressedListener != null) { mOnCommentPressedListener.onCommentPressed(getAdapterPosition(), v); } } @Override public boolean onLongClick(View v) { if (mOnCommentPressedListener != null) { mOnCommentPressedListener.onCommentLongPressed(getAdapterPosition(), v); } return true; } } CommentAdapter(Context context, SiteModel site) { ((WordPress) context.getApplicationContext()).component().inject(this); mInflater = LayoutInflater.from(context); mContext = context; mSite = site; mStatusColorSpam = ContextCompat.getColor(context, R.color.comment_status_spam); mStatusColorUnapproved = ContextCompat.getColor(context, R.color.comment_status_unapproved); mUnselectedColor = ContextCompat.getColor(context, R.color.white); mSelectedColor = ContextCompat.getColor(context, R.color.grey_lighten_20_translucent_50); mStatusTextSpam = context.getResources().getString(R.string.comment_status_spam); mStatusTextUnapproved = context.getResources().getString(R.string.comment_status_unapproved); mAvatarSz = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_medium); setHasStableIds(true); } void setOnDataLoadedListener(OnDataLoadedListener listener) { mOnDataLoadedListener = listener; } void setOnLoadMoreListener(OnLoadMoreListener listener) { mOnLoadMoreListener = listener; } void setOnCommentPressedListener(OnCommentPressedListener listener) { mOnCommentPressedListener = listener; } void setOnSelectedItemsChangeListener(OnSelectedItemsChangeListener listener) { mOnSelectedChangeListener = listener; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = mInflater.inflate(R.layout.comment_listitem, null); CommentHolder holder = new CommentHolder(view); view.setTag(holder); return holder; } private String getFormattedTitle(CommentModel comment) { String formattedTitle; Context context = WordPress.getContext(); String author = context.getString(R.string.anonymous); if (!TextUtils.isEmpty(comment.getAuthorName())) { author = StringUtils.unescapeHTML(comment.getAuthorName().trim()); } if (!TextUtils.isEmpty(comment.getPostTitle())) { formattedTitle = author + "<font color=" + HtmlUtils.colorResToHtmlColor(context, R.color.grey_darken_10) + ">" + " " + context.getString(R.string.on) + " " + "</font>" + StringUtils.unescapeHTML(comment.getPostTitle().trim()); } else { formattedTitle = author; } return formattedTitle; } private String getAvatarForDisplay(CommentModel comment, int avatarSize) { String avatarForDisplay = ""; if (!TextUtils.isEmpty(comment.getAuthorProfileImageUrl())) { avatarForDisplay = GravatarUtils.fixGravatarUrl(comment.getAuthorProfileImageUrl(), avatarSize); } else if (!TextUtils.isEmpty(comment.getAuthorEmail())) { avatarForDisplay = GravatarUtils.gravatarFromEmail(comment.getAuthorEmail(), avatarSize); } return avatarForDisplay; } private Spanned getSpannedContent(CommentModel comment) { String content = StringUtils.notNullStr(comment.getContent()); return WPHtml.fromHtml(content, null, null, mContext, null, 0); } private String getFormattedDate(CommentModel comment, Context context) { if (comment.getDatePublished() != null) { return DateTimeUtils.javaDateToTimeSpan(DateTimeUtils.dateFromIso8601(comment.getDatePublished()), context); } return ""; } @Override public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { CommentModel comment = mComments.get(position); CommentHolder holder = (CommentHolder) viewHolder; // Note: following operation can take some time, we could maybe cache the calculated objects (title, spanned // content) to make the list scroll smoother. holder.txtTitle.setText(Html.fromHtml(getFormattedTitle(comment))); holder.txtComment.setText(getSpannedContent(comment)); holder.txtDate.setText(getFormattedDate(comment, mContext)); // status is only shown for comments that haven't been approved final boolean showStatus; CommentStatus commentStatus = CommentStatus.fromString(comment.getStatus()); switch (commentStatus) { case SPAM: showStatus = true; holder.txtStatus.setText(mStatusTextSpam); holder.txtStatus.setTextColor(mStatusColorSpam); break; case UNAPPROVED: showStatus = true; holder.txtStatus.setText(mStatusTextUnapproved); holder.txtStatus.setTextColor(mStatusColorUnapproved); break; default: showStatus = false; break; } holder.txtStatus.setVisibility(showStatus ? View.VISIBLE : View.GONE); int checkmarkVisibility; if (mEnableSelection && isItemSelected(position)) { checkmarkVisibility = View.VISIBLE; holder.containerView.setBackgroundColor(mSelectedColor); } else { checkmarkVisibility = View.GONE; holder.imgAvatar.setImageUrl(getAvatarForDisplay(comment, mAvatarSz), WPNetworkImageView.ImageType.AVATAR); holder.containerView.setBackgroundColor(mUnselectedColor); } if (holder.imgCheckmark.getVisibility() != checkmarkVisibility) { holder.imgCheckmark.setVisibility(checkmarkVisibility); } // comment text needs to be to the left of date/status when the title is a single line and // the status is displayed or else the status may overlap the comment text - note that // getLineCount() will return 0 if the view hasn't been rendered yet, which is why we // check getLineCount() <= 1 boolean adjustComment = (showStatus && holder.txtTitle.getLineCount() <= 1); RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.txtComment.getLayoutParams(); if (adjustComment) { params.addRule(RelativeLayout.LEFT_OF, R.id.layout_date_status); } else { params.addRule(RelativeLayout.LEFT_OF, 0); } // request to load more comments when we near the end if (mOnLoadMoreListener != null && position >= getItemCount()-1 && position >= CommentsListFragment.COMMENTS_PER_PAGE - 1) { mOnLoadMoreListener.onLoadMore(); } } public CommentModel getItem(int position) { if (isPositionValid(position)) { return mComments.get(position); } else { return null; } } @Override public long getItemId(int position) { return mComments.get(position).getRemoteCommentId(); } @Override public int getItemCount() { return mComments.size(); } private boolean isEmpty() { return getItemCount() == 0; } void setEnableSelection(boolean enable) { if (enable == mEnableSelection) return; mEnableSelection = enable; if (mEnableSelection) { notifyDataSetChanged(); } else { clearSelectedComments(); } } void clearSelectedComments() { if (mSelectedPositions.size() > 0) { mSelectedPositions.clear(); notifyDataSetChanged(); if (mOnSelectedChangeListener != null) { mOnSelectedChangeListener.onSelectedItemsChanged(); } } } int getSelectedCommentCount() { return mSelectedPositions.size(); } CommentList getSelectedComments() { CommentList comments = new CommentList(); if (!mEnableSelection) { return comments; } for (Integer position: mSelectedPositions) { if (isPositionValid(position)) comments.add(mComments.get(position)); } return comments; } private boolean isItemSelected(int position) { return mSelectedPositions.contains(position); } void setItemSelected(int position, boolean isSelected, View view) { if (isItemSelected(position) == isSelected) return; if (isSelected) { mSelectedPositions.add(position); } else { mSelectedPositions.remove(position); } notifyItemChanged(position); if (view != null && view.getTag() instanceof CommentHolder) { CommentHolder holder = (CommentHolder) view.getTag(); // animate the selection change AniUtils.startAnimation(holder.imgCheckmark, isSelected ? R.anim.cab_select : R.anim.cab_deselect); holder.imgCheckmark.setVisibility(isSelected ? View.VISIBLE : View.GONE); } if (mOnSelectedChangeListener != null) { mOnSelectedChangeListener.onSelectedItemsChanged(); } } void toggleItemSelected(int position, View view) { setItemSelected(position, !isItemSelected(position), view); } private int indexOfCommentId(long commentId) { return mComments.indexOfCommentId(commentId); } private boolean isPositionValid(int position) { return (position >= 0 && position < mComments.size()); } public void removeComment(CommentModel comment) { int position = indexOfCommentId(comment.getRemoteCommentId()); if (position >= 0) { mComments.remove(position); notifyItemRemoved(position); } } /* * load comments using an AsyncTask */ void loadComments(CommentStatus statusFilter) { if (mIsLoadTaskRunning) { AppLog.w(AppLog.T.COMMENTS, "load comments task already active"); } else { new LoadCommentsTask(statusFilter).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } /* * AsyncTask to load comments from SQLite */ private boolean mIsLoadTaskRunning = false; private class LoadCommentsTask extends AsyncTask<Void, Void, Boolean> { CommentList tmpComments; final CommentStatus mStatusFilter; public LoadCommentsTask(CommentStatus statusFilter) { mStatusFilter = statusFilter; } @Override protected void onPreExecute() { mIsLoadTaskRunning = true; } @Override protected void onCancelled() { mIsLoadTaskRunning = false; } @Override protected Boolean doInBackground(Void... params) { List<CommentModel> comments; if (mStatusFilter == null || mStatusFilter == CommentStatus.ALL) { // The "all" filter actually means "approved" + "unapproved" (but not "spam", "trash" or "deleted") comments = mCommentStore.getCommentsForSite(mSite, false, CommentStatus.APPROVED, CommentStatus.UNAPPROVED); } else { comments = mCommentStore.getCommentsForSite(mSite, false, mStatusFilter); } tmpComments = new CommentList(); tmpComments.addAll(comments); return !mComments.isSameList(tmpComments); } @Override protected void onPostExecute(Boolean result) { if (result) { mComments.clear(); mComments.addAll(tmpComments); // Sort by date Collections.sort(mComments, new Comparator<CommentModel>() { @Override public int compare(CommentModel commentModel, CommentModel t1) { Date d0 = DateTimeUtils.dateFromIso8601(commentModel.getDatePublished()); Date d1 = DateTimeUtils.dateFromIso8601(t1.getDatePublished()); if (d0 == null || d1 == null) { return 0; } return d1.compareTo(d0); } }); notifyDataSetChanged(); } if (mOnDataLoadedListener != null) { mOnDataLoadedListener.onDataLoaded(isEmpty()); } mIsLoadTaskRunning = false; } } }