aboutsummaryrefslogtreecommitdiff
path: root/WordPress/src/main/java/org/wordpress/android/ui/comments
diff options
context:
space:
mode:
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/comments')
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActionResult.java18
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActions.java503
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapter.java486
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapterState.java69
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailActivity.java92
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java1234
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDialogs.java52
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentEvents.java56
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentUtils.java107
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsActivity.java314
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsListFragment.java795
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/comments/EditCommentActivity.java440
12 files changed, 4166 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActionResult.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActionResult.java
new file mode 100644
index 000000000..41178da31
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActionResult.java
@@ -0,0 +1,18 @@
+package org.wordpress.android.ui.comments;
+
+public class CommentActionResult {
+
+ public static final int COMMENT_ID_ON_ERRORS = -1;
+ public static final int COMMENT_ID_UNKNOWN = -2; // This is used primarily for replies, when the commentID isn't known.
+
+ private long mCommentID = COMMENT_ID_UNKNOWN;
+ private final String mMessage;
+
+ public CommentActionResult(long commentID, String message) {
+ mCommentID = commentID;
+ mMessage = message;
+ }
+
+ public String getMessage() { return mMessage; }
+ public boolean isSuccess() { return mCommentID != COMMENT_ID_ON_ERRORS; }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActions.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActions.java
new file mode 100644
index 000000000..acc83e704
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentActions.java
@@ -0,0 +1,503 @@
+package org.wordpress.android.ui.comments;
+
+import android.os.Handler;
+import android.text.TextUtils;
+
+import com.android.volley.VolleyError;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.CommentTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentList;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.VolleyUtils;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCException;
+import org.xmlrpc.android.XMLRPCFactory;
+import org.xmlrpc.android.XMLRPCFault;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * actions related to comments - replies, moderating, etc.
+ * methods below do network calls in the background & update local DB upon success
+ * all methods below MUST be called from UI thread
+ */
+
+public class CommentActions {
+
+ private CommentActions() {
+ throw new AssertionError();
+ }
+
+ /*
+ * listener when a comment action is performed
+ */
+ public interface CommentActionListener {
+ void onActionResult(CommentActionResult result);
+ }
+
+ /*
+ * listener when comments are moderated or deleted
+ */
+ public interface OnCommentsModeratedListener {
+ void onCommentsModerated(final CommentList moderatedComments);
+ }
+
+ /*
+ * used by comment fragments to alert container activity of a change to one or more
+ * comments (moderated, deleted, added, etc.)
+ */
+ public enum ChangeType {EDITED, REPLIED}
+ public interface OnCommentChangeListener {
+ void onCommentChanged(ChangeType changeType);
+ }
+
+ public interface OnCommentActionListener {
+ void onModerateComment(int accountId, Comment comment, CommentStatus newStatus);
+ }
+
+ public interface OnNoteCommentActionListener {
+ void onModerateCommentForNote(Note note, CommentStatus newStatus);
+ }
+
+
+ /**
+ * reply to an individual comment
+ */
+ static void submitReplyToComment(final int accountId,
+ final Comment comment,
+ final String replyText,
+ final CommentActionListener actionListener) {
+ final Blog blog = WordPress.getBlog(accountId);
+ if (blog==null || comment==null || TextUtils.isEmpty(replyText)) {
+ if (actionListener != null) {
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+ }
+ return;
+ }
+
+ final Handler handler = new Handler();
+
+ new Thread() {
+ @Override
+ public void run() {
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+
+ Map<String, Object> replyHash = new HashMap<>();
+ replyHash.put("comment_parent", Long.toString(comment.commentID));
+ replyHash.put("content", replyText);
+ replyHash.put("author", "");
+ replyHash.put("author_url", "");
+ replyHash.put("author_email", "");
+
+ Object[] params = {
+ blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ Long.toString(comment.postID),
+ replyHash };
+
+ long newCommentID;
+ String message = null;
+ try {
+ Object newCommentIDObject = client.call(Method.NEW_COMMENT, params);
+ if (newCommentIDObject instanceof Integer) {
+ newCommentID = ((Integer) newCommentIDObject).longValue();
+ } else if (newCommentIDObject instanceof Long) {
+ newCommentID = (Long) newCommentIDObject;
+ } else {
+ AppLog.e(T.COMMENTS, "wp.newComment returned the wrong data type");
+ newCommentID = CommentActionResult.COMMENT_ID_ON_ERRORS;
+ }
+ } catch (XMLRPCFault e) {
+ AppLog.e(T.COMMENTS, "Error while sending the new comment", e);
+ newCommentID = CommentActionResult.COMMENT_ID_ON_ERRORS;
+ message = e.getFaultString();
+ } catch (XMLRPCException | IOException | XmlPullParserException e) {
+ AppLog.e(T.COMMENTS, "Error while sending the new comment", e);
+ newCommentID = CommentActionResult.COMMENT_ID_ON_ERRORS;
+ }
+
+ final CommentActionResult cr = new CommentActionResult(newCommentID, message);
+
+ if (actionListener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ actionListener.onActionResult(cr);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ /**
+ * reply to an individual comment that came from a notification - this differs from
+ * submitReplyToComment() in that it enables responding to a reply to a comment this
+ * user made on someone else's blog
+ */
+ public static void submitReplyToCommentNote(final Note note,
+ final String replyText,
+ final CommentActionListener actionListener) {
+ if (note == null || TextUtils.isEmpty(replyText)) {
+ if (actionListener != null)
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+
+ return;
+ }
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (actionListener != null)
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_UNKNOWN, null));
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ if (volleyError != null)
+ AppLog.e(T.COMMENTS, volleyError.getMessage(), volleyError);
+ if (actionListener != null) {
+ actionListener.onActionResult(
+ new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, VolleyUtils.messageStringFromVolleyError(volleyError))
+ );
+ }
+ }
+ };
+
+ Note.Reply reply = note.buildReply(replyText);
+ WordPress.getRestClientUtils().replyToComment(reply.getContent(), reply.getRestPath(), listener, errorListener);
+ }
+
+ /**
+ * reply to an individual comment via the WP.com REST API
+ */
+ public static void submitReplyToCommentRestApi(long siteId, long commentId,
+ final String replyText,
+ final CommentActionListener actionListener) {
+ if (TextUtils.isEmpty(replyText)) {
+ if (actionListener != null)
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+ return;
+ }
+
+ RestRequest.Listener listener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (actionListener != null)
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_UNKNOWN, null));
+ }
+ };
+ RestRequest.ErrorListener errorListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ if (volleyError != null)
+ AppLog.e(T.COMMENTS, volleyError.getMessage(), volleyError);
+ if (actionListener != null)
+ actionListener.onActionResult(
+ new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, VolleyUtils.messageStringFromVolleyError(volleyError))
+ );
+ }
+ };
+
+ WordPress.getRestClientUtils().replyToComment(siteId, commentId, replyText, listener, errorListener);
+ }
+
+ /**
+ * Moderate a comment from a WPCOM notification
+ */
+ public static void moderateCommentRestApi(long siteId,
+ final long commentId,
+ CommentStatus newStatus,
+ final CommentActionListener actionListener) {
+
+ WordPress.getRestClientUtils().moderateComment(
+ String.valueOf(siteId),
+ String.valueOf(commentId),
+ CommentStatus.toRESTString(newStatus),
+ new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (actionListener != null) {
+ actionListener.onActionResult(new CommentActionResult(commentId, null));
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (actionListener != null) {
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * Moderate a comment from a WPCOM notification
+ */
+ public static void moderateCommentForNote(final Note note, CommentStatus newStatus,
+ final CommentActionListener actionListener) {
+ WordPress.getRestClientUtils().moderateComment(
+ String.valueOf(note.getSiteId()),
+ String.valueOf(note.getCommentId()),
+ CommentStatus.toRESTString(newStatus),
+ new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (actionListener != null) {
+ actionListener.onActionResult(new CommentActionResult(note.getCommentId(), null));
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (actionListener != null) {
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+ }
+ }
+ }
+ );
+ }
+
+ /**
+ * change the status of a single comment
+ */
+ static void moderateComment(final int accountId,
+ final Comment comment,
+ final CommentStatus newStatus,
+ final CommentActionListener actionListener) {
+ // deletion is handled separately
+ if (newStatus != null && (newStatus.equals(CommentStatus.TRASH) || newStatus.equals(CommentStatus.DELETE))) {
+ deleteComment(accountId, comment, actionListener, newStatus.equals(CommentStatus.DELETE));
+ return;
+ }
+
+ final Blog blog = WordPress.getBlog(accountId);
+
+ if (blog==null || comment==null || newStatus==null || newStatus==CommentStatus.UNKNOWN) {
+ if (actionListener != null)
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+ return;
+ }
+
+ final Handler handler = new Handler();
+
+ new Thread() {
+ @Override
+ public void run() {
+ final boolean success = ApiHelper.editComment(blog, comment, newStatus);
+
+ if (success) {
+ CommentTable.updateCommentStatus(blog.getLocalTableBlogId(), comment.commentID, CommentStatus
+ .toString(newStatus));
+ }
+
+ if (actionListener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ actionListener.onActionResult(new CommentActionResult(comment.commentID, null));
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ /**
+ * change the status of multiple comments
+ * TODO: investigate using system.multiCall to perform a single call to moderate the list
+ */
+ static void moderateComments(final int accountId,
+ final CommentList comments,
+ final CommentStatus newStatus,
+ final OnCommentsModeratedListener actionListener) {
+ // deletion is handled separately
+ if (newStatus != null && (newStatus.equals(CommentStatus.TRASH) || newStatus.equals(CommentStatus.DELETE))) {
+ deleteComments(accountId, comments, actionListener, newStatus.equals(CommentStatus.DELETE));
+ return;
+ }
+
+ final Blog blog = WordPress.getBlog(accountId);
+
+ if (blog==null || comments==null || comments.size() == 0 || newStatus==null || newStatus==CommentStatus.UNKNOWN) {
+ if (actionListener != null)
+ actionListener.onCommentsModerated(new CommentList());
+ return;
+ }
+
+ final CommentList moderatedComments = new CommentList();
+ final String newStatusStr = CommentStatus.toString(newStatus);
+ final int localBlogId = blog.getLocalTableBlogId();
+
+ final Handler handler = new Handler();
+ new Thread() {
+ @Override
+ public void run() {
+ for (Comment comment: comments) {
+ if (ApiHelper.editComment(blog, comment, newStatus)) {
+ comment.setStatus(newStatusStr);
+ moderatedComments.add(comment);
+ }
+ }
+
+ // update status in SQLite of successfully moderated comments
+ CommentTable.updateCommentsStatus(localBlogId, moderatedComments, newStatusStr);
+
+ if (actionListener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ actionListener.onCommentsModerated(moderatedComments);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ /**
+ * delete (trash) a single comment
+ */
+ private static void deleteComment(final int accountId,
+ final Comment comment,
+ final CommentActionListener actionListener,
+ final boolean deletePermanently) {
+ final Blog blog = WordPress.getBlog(accountId);
+ if (blog==null || comment==null) {
+ if (actionListener != null)
+ actionListener.onActionResult(new CommentActionResult(CommentActionResult.COMMENT_ID_ON_ERRORS, null));
+ return;
+ }
+
+ final Handler handler = new Handler();
+
+ new Thread() {
+ @Override
+ public void run() {
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+
+ Object[] params = {
+ blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ comment.commentID,
+ deletePermanently};
+
+ Object result;
+ try {
+ result = client.call(Method.DELETE_COMMENT, params);
+ } catch (final XMLRPCException | XmlPullParserException | IOException e) {
+ AppLog.e(T.COMMENTS, "Error while deleting comment", e);
+ result = null;
+ }
+
+ //update local database
+ final boolean success = (result != null && Boolean.parseBoolean(result.toString()));
+ if (success){
+ if (deletePermanently) {
+ CommentTable.deleteComment(accountId, comment.commentID);
+ }
+ else {
+ // update status in SQLite of successfully moderated comments
+ CommentTable.updateCommentStatus(blog.getLocalTableBlogId(), comment.commentID,
+ CommentStatus.toString(CommentStatus.TRASH));
+ }
+ }
+
+ if (actionListener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ actionListener.onActionResult(new CommentActionResult(comment.commentID, null));
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ /**
+ * delete multiple comments
+ */
+ private static void deleteComments(final int accountId,
+ final CommentList comments,
+ final OnCommentsModeratedListener actionListener,
+ final boolean deletePermanently) {
+ final Blog blog = WordPress.getBlog(accountId);
+
+ if (blog==null || comments==null || comments.size() == 0) {
+ if (actionListener != null)
+ actionListener.onCommentsModerated(new CommentList());
+ return;
+ }
+
+ final CommentList deletedComments = new CommentList();
+ final int localBlogId = blog.getLocalTableBlogId();
+ final int remoteBlogId = blog.getRemoteBlogId();
+
+ final Handler handler = new Handler();
+ new Thread() {
+ @Override
+ public void run() {
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+
+ for (Comment comment: comments) {
+ Object[] params = {
+ remoteBlogId,
+ blog.getUsername(),
+ blog.getPassword(),
+ comment.commentID,
+ deletePermanently};
+
+ Object result;
+ try {
+ result = client.call(Method.DELETE_COMMENT, params);
+ boolean success = (result != null && Boolean.parseBoolean(result.toString()));
+ if (success)
+ deletedComments.add(comment);
+ } catch (XMLRPCException | XmlPullParserException | IOException e) {
+ AppLog.e(T.COMMENTS, "Error while deleting comment", e);
+ }
+ }
+
+ // remove successfully deleted comments from SQLite
+ if (deletePermanently) {
+ CommentTable.deleteComments(localBlogId, deletedComments);
+ }
+ else {
+ // update status in SQLite of successfully moderated comments
+ CommentTable.updateCommentsStatus(blog.getLocalTableBlogId(), deletedComments,
+ CommentStatus.toString(CommentStatus.TRASH));
+ }
+
+ if (actionListener != null) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ actionListener.onCommentsModerated(deletedComments);
+ }
+ });
+ }
+ }
+ }.start();
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapter.java
new file mode 100644
index 000000000..d5e5bd6fb
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapter.java
@@ -0,0 +1,486 @@
+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.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.datasets.CommentTable;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentList;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.WPHtml;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.HashSet;
+
+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<Long> mSelectedCommentsId = new HashSet<>();
+ private final HashSet<Long> mModeratingCommentsIds = new HashSet<>();
+
+ private final int mStatusColorSpam;
+ private final int mStatusColorUnapproved;
+
+ private final int mLocalBlogId;
+ 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;
+
+ 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 View progressBar;
+ 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);
+ progressBar = view.findViewById(R.id.moderate_progress);
+ 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, int localBlogId) {
+ mInflater = LayoutInflater.from(context);
+ mContext = context;
+
+ mLocalBlogId = localBlogId;
+
+ 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.translucent_grey_lighten_20);
+
+ 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;
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
+ Comment comment = mComments.get(position);
+ CommentHolder holder = (CommentHolder) viewHolder;
+
+ if (isModeratingCommentId(comment.commentID)) {
+ holder.progressBar.setVisibility(View.VISIBLE);
+ } else {
+ holder.progressBar.setVisibility(View.GONE);
+ }
+
+ holder.txtTitle.setText(Html.fromHtml(comment.getFormattedTitle()));
+ holder.txtComment.setText(comment.getUnescapedCommentTextWithDrawables());
+ holder.txtDate.setText(DateTimeUtils.javaDateToTimeSpan(comment.getDatePublished(), mContext));
+
+ // status is only shown for comments that haven't been approved
+ final boolean showStatus;
+ switch (comment.getStatusEnum()) {
+ 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(comment.getAvatarForDisplay(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 Comment getItem(int position) {
+ if (isPositionValid(position)) {
+ return mComments.get(position);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mComments.get(position).commentID;
+ }
+
+ @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 (mSelectedCommentsId.size() > 0) {
+ mSelectedCommentsId.clear();
+ notifyDataSetChanged();
+ if (mOnSelectedChangeListener != null) {
+ mOnSelectedChangeListener.onSelectedItemsChanged();
+ }
+ }
+ }
+
+ int getSelectedCommentCount() {
+ return mSelectedCommentsId.size();
+ }
+
+ CommentList getSelectedComments() {
+ CommentList comments = new CommentList();
+ if (!mEnableSelection) {
+ return comments;
+ }
+
+ for (Long commentId : mSelectedCommentsId) {
+ int commentIndex = indexOfCommentId(commentId);
+ if (commentIndex > -1) {
+ comments.add(mComments.get(commentIndex));
+ }
+ }
+
+ return comments;
+ }
+
+ private boolean isItemSelected(int position) {
+ Comment comment = getItem(position);
+ return comment != null && mSelectedCommentsId.contains(comment.commentID);
+ }
+
+ void setItemSelected(int position, boolean isSelected, View view) {
+ if (isItemSelected(position) == isSelected) return;
+
+ Comment comment = getItem(position);
+ if (comment == null) return;
+
+ if (isSelected) {
+ mSelectedCommentsId.add(comment.commentID);
+ } else {
+ mSelectedCommentsId.remove(comment.commentID);
+ }
+
+
+ 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);
+ }
+
+ public void addModeratingCommentId(long commentId) {
+ mModeratingCommentsIds.add(commentId);
+ int position = indexOfCommentId(commentId);
+ if (position >= 0) {
+ notifyItemChanged(position);
+ }
+ }
+
+ public void removeModeratingCommentId(long commentId) {
+ mModeratingCommentsIds.remove(commentId);
+ int position = indexOfCommentId(commentId);
+ if (position >= 0) {
+ notifyItemChanged(position);
+ }
+ }
+
+ public boolean isModeratingCommentId(long commentId) {
+ return mModeratingCommentsIds.size() > 0
+ && mModeratingCommentsIds.contains(commentId);
+ }
+
+ private int indexOfCommentId(long commentId) {
+ return mComments.indexOfCommentId(commentId);
+ }
+
+ private boolean isPositionValid(int position) {
+ return (position >= 0 && position < mComments.size());
+ }
+
+ void replaceComments(CommentList comments) {
+ mComments.replaceComments(comments);
+ notifyDataSetChanged();
+ }
+
+ void deleteComments(CommentList comments) {
+ mComments.deleteComments(comments);
+ notifyDataSetChanged();
+ if (mOnDataLoadedListener != null) {
+ mOnDataLoadedListener.onDataLoaded(isEmpty());
+ }
+ }
+
+ public void removeComment(Comment comment) {
+ int position = indexOfCommentId(comment.commentID);
+ if (position >= 0) {
+ mComments.remove(position);
+ notifyItemRemoved(position);
+ }
+ }
+
+ /*
+ * clear all comments
+ */
+ void clearComments() {
+ if (mComments != null) {
+ mComments.clear();
+ notifyDataSetChanged();
+ }
+ }
+
+ /*
+ * 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) {
+ if (mStatusFilter == null) {
+ tmpComments = CommentTable.getCommentsForBlogWithFilter(mLocalBlogId, CommentStatus.UNKNOWN);
+ } else {
+ tmpComments = CommentTable.getCommentsForBlogWithFilter(mLocalBlogId, mStatusFilter);
+ }
+
+ if (mComments.isSameList(tmpComments)) {
+ return false;
+ }
+
+ // pre-calc transient values so they're cached prior to display
+ for (Comment comment : tmpComments) {
+ comment.getDatePublished();
+ comment.getUnescapedPostTitle();
+ comment.getAvatarForDisplay(mAvatarSz);
+ comment.getFormattedTitle();
+
+ String content = StringUtils.notNullStr(comment.getCommentText());
+ //to load images embedded within comments, pass an ImageGetter to WPHtml.fromHtml()
+ Spanned spanned = WPHtml.fromHtml(content, null, null, mContext, null, 0);
+ comment.setUnescapedCommentWithDrawables(spanned);
+ }
+
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result) {
+ mComments.clear();
+ mComments.addAll(tmpComments);
+ notifyDataSetChanged();
+ }
+
+ if (mOnDataLoadedListener != null) {
+ mOnDataLoadedListener.onDataLoaded(isEmpty());
+ }
+
+ mIsLoadTaskRunning = false;
+ }
+ }
+
+ public HashSet<Long> getSelectedCommentsId() {
+ return mSelectedCommentsId;
+ }
+
+
+ public CommentAdapterState getAdapterState() {
+ return new CommentAdapterState(mSelectedCommentsId, mModeratingCommentsIds);
+ }
+
+ public void setInitialState(CommentAdapterState adapterState) {
+ if (adapterState == null) return;
+
+ if (adapterState.hasSelectedComments()) {
+ mSelectedCommentsId.clear();
+ mSelectedCommentsId.addAll(adapterState.getSelectedComments());
+ setEnableSelection(true);
+ }
+
+ if (adapterState.hasModeratingComments()) {
+ mModeratingCommentsIds.clear();
+ mModeratingCommentsIds.addAll(adapterState.getModeratedCommentsId());
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapterState.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapterState.java
new file mode 100644
index 000000000..6d9ab1530
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentAdapterState.java
@@ -0,0 +1,69 @@
+package org.wordpress.android.ui.comments;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+
+import java.util.HashSet;
+
+/**
+ * Used to store state of {@link CommentAdapter}
+ */
+public class CommentAdapterState implements Parcelable {
+ public static final String KEY = "comments_adapter_state";
+
+ private final HashSet<Long> mSelectedComments;
+ private final HashSet<Long> mModeratedCommentsId;
+
+ public CommentAdapterState(@NonNull HashSet<Long> selectedComments, @NonNull HashSet<Long> moderatedCommentsId) {
+ mSelectedComments = selectedComments;
+ mModeratedCommentsId = moderatedCommentsId;
+ }
+
+ public HashSet<Long> getSelectedComments() {
+ return mSelectedComments;
+ }
+
+ public HashSet<Long> getModeratedCommentsId() {
+ return mModeratedCommentsId;
+ }
+
+
+ public boolean hasSelectedComments() {
+ return mSelectedComments != null && mSelectedComments.size() > 0;
+ }
+
+ public boolean hasModeratingComments() {
+ return mModeratedCommentsId != null && mModeratedCommentsId.size() > 0;
+ }
+
+ @SuppressWarnings("unchecked")
+ private CommentAdapterState(Parcel in) {
+ mSelectedComments = (HashSet<Long>) in.readSerializable();
+ mModeratedCommentsId = (HashSet<Long>) in.readSerializable();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeSerializable(mSelectedComments);
+ dest.writeSerializable(mModeratedCommentsId);
+ }
+
+ @SuppressWarnings("unused")
+ public static final Parcelable.Creator<CommentAdapterState> CREATOR = new Parcelable.Creator<CommentAdapterState>() {
+ @Override
+ public CommentAdapterState createFromParcel(Parcel in) {
+ return new CommentAdapterState(in);
+ }
+
+ @Override
+ public CommentAdapterState[] newArray(int size) {
+ return new CommentAdapterState[size];
+ }
+ };
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailActivity.java
new file mode 100644
index 000000000..69e8938e1
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailActivity.java
@@ -0,0 +1,92 @@
+package org.wordpress.android.ui.comments;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+
+import com.simperium.client.BucketObjectMissingException;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.ToastUtils;
+
+// simple wrapper activity for CommentDetailFragment
+public class CommentDetailActivity extends AppCompatActivity {
+
+ private static final String KEY_COMMENT_DETAIL_LOCAL_TABLE_BLOG_ID = "local_table_blog_id";
+ private static final String KEY_COMMENT_DETAIL_COMMENT_ID = "comment_detail_comment_id";
+ private static final String KEY_COMMENT_DETAIL_NOTE_ID = "comment_detail_note_id";
+ private static final String KEY_COMMENT_DETAIL_IS_REMOTE = "comment_detail_is_remote";
+
+ private static final String TAG_COMMENT_DETAIL_FRAGMENT = "tag_comment_detail_fragment";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.comment_activity_detail);
+
+ setTitle(R.string.comment);
+
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+
+ if (savedInstanceState == null) {
+ Intent intent = getIntent();
+ CommentDetailFragment commentDetailFragment = null;
+ if (intent.getStringExtra(KEY_COMMENT_DETAIL_NOTE_ID) != null && SimperiumUtils.getNotesBucket() != null) {
+ try {
+ Note note = SimperiumUtils.getNotesBucket().get(
+ intent.getStringExtra(KEY_COMMENT_DETAIL_NOTE_ID)
+ );
+
+ if (intent.hasExtra(KEY_COMMENT_DETAIL_IS_REMOTE)) {
+ commentDetailFragment = CommentDetailFragment.newInstanceForRemoteNoteComment(note.getId());
+ } else {
+ commentDetailFragment = CommentDetailFragment.newInstance(note.getId());
+ }
+ } catch (BucketObjectMissingException e) {
+ AppLog.e(AppLog.T.NOTIFS, "CommentDetailActivity was passed an invalid note id.");
+ }
+ } else if (intent.getIntExtra(KEY_COMMENT_DETAIL_LOCAL_TABLE_BLOG_ID, 0) > 0
+ && intent.getLongExtra(KEY_COMMENT_DETAIL_COMMENT_ID, 0) > 0) {
+ commentDetailFragment = CommentDetailFragment.newInstance(
+ intent.getIntExtra(KEY_COMMENT_DETAIL_LOCAL_TABLE_BLOG_ID, 0),
+ intent.getLongExtra(KEY_COMMENT_DETAIL_COMMENT_ID, 0)
+ );
+ }
+
+ if (commentDetailFragment != null) {
+ commentDetailFragment.setRetainInstance(true);
+ getFragmentManager().beginTransaction()
+ .add(R.id.comment_detail_container, commentDetailFragment, TAG_COMMENT_DETAIL_FRAGMENT)
+ .commit();
+ } else {
+ ToastUtils.showToast(this, R.string.error_load_comment);
+ finish();
+ }
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ActivityId.trackLastActivity(ActivityId.COMMENT_DETAIL);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java
new file mode 100644
index 000000000..eafe02723
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java
@@ -0,0 +1,1234 @@
+package org.wordpress.android.ui.comments;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.text.Html;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import com.android.volley.VolleyError;
+import com.simperium.client.BucketObjectMissingException;
+import com.wordpress.rest.RestRequest;
+
+import org.apache.commons.lang.StringEscapeUtils;
+import org.json.JSONObject;
+import org.wordpress.android.Constants;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.analytics.AnalyticsTracker;
+import org.wordpress.android.analytics.AnalyticsTracker.Stat;
+import org.wordpress.android.datasets.CommentTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.datasets.SuggestionTable;
+import org.wordpress.android.models.AccountHelper;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.models.Note.EnabledActions;
+import org.wordpress.android.models.Suggestion;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.comments.CommentActions.ChangeType;
+import org.wordpress.android.ui.comments.CommentActions.OnCommentActionListener;
+import org.wordpress.android.ui.comments.CommentActions.OnCommentChangeListener;
+import org.wordpress.android.ui.comments.CommentActions.OnNoteCommentActionListener;
+import org.wordpress.android.ui.notifications.NotificationFragment;
+import org.wordpress.android.ui.notifications.NotificationsDetailListFragment;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderAnim;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderPostActions;
+import org.wordpress.android.ui.suggestion.adapters.SuggestionAdapter;
+import org.wordpress.android.ui.suggestion.service.SuggestionEvents;
+import org.wordpress.android.ui.suggestion.util.SuggestionServiceConnectionManager;
+import org.wordpress.android.ui.suggestion.util.SuggestionUtils;
+import org.wordpress.android.util.AniUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.DateTimeUtils;
+import org.wordpress.android.util.EditTextUtils;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.LanguageUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.VolleyUtils;
+import org.wordpress.android.util.WPLinkMovementMethod;
+import org.wordpress.android.widgets.SuggestionAutoCompleteText;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.EnumSet;
+import java.util.List;
+
+import de.greenrobot.event.EventBus;
+
+/**
+ * comment detail displayed from both the notification list and the comment list
+ * prior to this there were separate comment detail screens for each list
+ */
+public class CommentDetailFragment extends Fragment implements NotificationFragment {
+ private static final String KEY_LOCAL_BLOG_ID = "local_blog_id";
+ private static final String KEY_COMMENT_ID = "comment_id";
+ private static final String KEY_NOTE_ID = "note_id";
+ private int mLocalBlogId;
+ private int mRemoteBlogId;
+ private Comment mComment;
+ private Note mNote;
+ private SuggestionAdapter mSuggestionAdapter;
+ private SuggestionServiceConnectionManager mSuggestionServiceConnectionManager;
+ private TextView mTxtStatus;
+ private TextView mTxtContent;
+ private View mSubmitReplyBtn;
+ private SuggestionAutoCompleteText mEditReply;
+ private ViewGroup mLayoutReply;
+ private ViewGroup mLayoutButtons;
+ private View mBtnLikeComment;
+ private ImageView mBtnLikeIcon;
+ private TextView mBtnLikeTextView;
+ private View mBtnModerateComment;
+ private ImageView mBtnModerateIcon;
+ private TextView mBtnModerateTextView;
+ private TextView mBtnSpamComment;
+ private TextView mBtnTrashComment;
+ private String mRestoredReplyText;
+ private String mRestoredNoteId;
+ private boolean mIsUsersBlog = false;
+ private boolean mShouldFocusReplyField;
+ private boolean mShouldLikeInstantly;
+ private boolean mShouldApproveInstantly;
+
+ /*
+ * Used to request a comment from a note using its site and comment ids, rather than build
+ * the comment with the content in the note. See showComment()
+ */
+ private boolean mShouldRequestCommentFromNote = false;
+ private boolean mIsSubmittingReply = false;
+ private NotificationsDetailListFragment mNotificationsDetailListFragment;
+ private OnCommentChangeListener mOnCommentChangeListener;
+ private OnPostClickListener mOnPostClickListener;
+ private OnCommentActionListener mOnCommentActionListener;
+ private OnNoteCommentActionListener mOnNoteCommentActionListener;
+ /*
+ * these determine which actions (moderation, replying, marking as spam) to enable
+ * for this comment - all actions are enabled when opened from the comment list, only
+ * changed when opened from a notification
+ */
+ private EnumSet<EnabledActions> mEnabledActions = EnumSet.allOf(EnabledActions.class);
+
+ /*
+ * used when called from comment list
+ */
+ static CommentDetailFragment newInstance(int localBlogId, long commentId) {
+ CommentDetailFragment fragment = new CommentDetailFragment();
+ fragment.setComment(localBlogId, commentId);
+ return fragment;
+ }
+
+ /*
+ * used when called from notification list for a comment notification
+ */
+ public static CommentDetailFragment newInstance(final String noteId) {
+ CommentDetailFragment fragment = new CommentDetailFragment();
+ fragment.setNoteWithNoteId(noteId);
+ return fragment;
+ }
+
+ /*
+ * used when called from a comment notification 'like' action
+ */
+ public static CommentDetailFragment newInstanceForInstantLike(final String noteId) {
+ CommentDetailFragment fragment = newInstance(noteId);
+ //here tell the fragment to trigger the Like action when ready
+ fragment.setLikeCommentWhenReady();
+ return fragment;
+ }
+
+ /*
+ * used when called from a comment notification 'approve' action
+ */
+ public static CommentDetailFragment newInstanceForInstantApprove(final String noteId) {
+ CommentDetailFragment fragment = newInstance(noteId);
+ //here tell the fragment to trigger the Like action when ready
+ fragment.setApproveCommentWhenReady();
+ return fragment;
+ }
+
+ /*
+ * used when called from notifications to load a comment that doesn't already exist in the note
+ */
+ public static CommentDetailFragment newInstanceForRemoteNoteComment(final String noteId) {
+ CommentDetailFragment fragment = newInstance(noteId);
+ fragment.enableShouldRequestCommentFromNote();
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ if (savedInstanceState.getString(KEY_NOTE_ID) != null) {
+ // The note will be set in onResume() because Simperium will be running there
+ // See WordPress.deferredInit()
+ mRestoredNoteId = savedInstanceState.getString(KEY_NOTE_ID);
+ } else {
+ int localBlogId = savedInstanceState.getInt(KEY_LOCAL_BLOG_ID);
+ long commentId = savedInstanceState.getLong(KEY_COMMENT_ID);
+ setComment(localBlogId, commentId);
+ }
+ }
+
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (hasComment()) {
+ outState.putInt(KEY_LOCAL_BLOG_ID, getLocalBlogId());
+ outState.putLong(KEY_COMMENT_ID, getCommentId());
+ }
+
+ if (mNote != null) {
+ outState.putString(KEY_NOTE_ID, mNote.getId());
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mSuggestionServiceConnectionManager != null) {
+ mSuggestionServiceConnectionManager.unbindFromService();
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View view = inflater.inflate(R.layout.comment_detail_fragment, container, false);
+
+ mTxtStatus = (TextView) view.findViewById(R.id.text_status);
+ mTxtContent = (TextView) view.findViewById(R.id.text_content);
+
+ mLayoutButtons = (ViewGroup) inflater.inflate(R.layout.comment_action_footer, null, false);
+ mBtnLikeComment = mLayoutButtons.findViewById(R.id.btn_like);
+ mBtnLikeIcon = (ImageView) mLayoutButtons.findViewById(R.id.btn_like_icon);
+ mBtnLikeTextView = (TextView) mLayoutButtons.findViewById(R.id.btn_like_text);
+ mBtnModerateComment = mLayoutButtons.findViewById(R.id.btn_moderate);
+ mBtnModerateIcon = (ImageView) mLayoutButtons.findViewById(R.id.btn_moderate_icon);
+ mBtnModerateTextView = (TextView) mLayoutButtons.findViewById(R.id.btn_moderate_text);
+ mBtnSpamComment = (TextView) mLayoutButtons.findViewById(R.id.text_btn_spam);
+ mBtnTrashComment = (TextView) mLayoutButtons.findViewById(R.id.image_trash_comment);
+
+ setTextDrawable(mBtnSpamComment, R.drawable.ic_action_spam);
+ setTextDrawable(mBtnTrashComment, R.drawable.ic_action_trash);
+
+ mLayoutReply = (ViewGroup) view.findViewById(R.id.layout_comment_box);
+ mEditReply = (SuggestionAutoCompleteText) mLayoutReply.findViewById(R.id.edit_comment);
+ mEditReply.getAutoSaveTextHelper().setUniqueId(String.format(LanguageUtils.getCurrentDeviceLanguage(getActivity()), "%s%d%d",
+ AccountHelper.getCurrentUsernameForBlog(WordPress.getCurrentBlog()),
+ getRemoteBlogId(), getCommentId()));
+
+ mSubmitReplyBtn = mLayoutReply.findViewById(R.id.btn_submit_reply);
+
+ View replyBox = mLayoutReply.findViewById(R.id.reply_box);
+ if (mComment != null &&
+ (mComment.getStatusEnum() == CommentStatus.SPAM ||
+ mComment.getStatusEnum() == CommentStatus.TRASH ||
+ mComment.getStatusEnum() == CommentStatus.DELETE)) {
+ replyBox.setVisibility(View.GONE);
+ } else {
+ replyBox.setVisibility(View.VISIBLE);
+ }
+
+ // hide comment like button until we know it can be enabled in showCommentForNote()
+ mBtnLikeComment.setVisibility(View.GONE);
+
+ // hide moderation buttons until updateModerationButtons() is called
+ mLayoutButtons.setVisibility(View.GONE);
+
+ // this is necessary in order for anchor tags in the comment text to be clickable
+ mTxtContent.setLinksClickable(true);
+ mTxtContent.setMovementMethod(WPLinkMovementMethod.getInstance());
+
+ mEditReply.setHint(R.string.reader_hint_comment_on_comment);
+ mEditReply.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_SEND)
+ submitReply();
+ return false;
+ }
+ });
+
+ if (!TextUtils.isEmpty(mRestoredReplyText)) {
+ mEditReply.setText(mRestoredReplyText);
+ mRestoredReplyText = null;
+ }
+
+ mSubmitReplyBtn.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ submitReply();
+ }
+ });
+
+ mBtnSpamComment.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!hasComment()) return;
+
+ if (mComment.getStatusEnum() == CommentStatus.SPAM) {
+ moderateComment(CommentStatus.APPROVED);
+ } else {
+ moderateComment(CommentStatus.SPAM);
+ }
+ }
+ });
+
+ mBtnTrashComment.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!hasComment()) return;
+
+ if (mComment.willTrashingPermanentlyDelete()) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(
+ getActivity());
+ dialogBuilder.setTitle(getResources().getText(R.string.delete));
+ dialogBuilder.setMessage(getResources().getText(R.string.dlg_sure_to_delete_comment));
+ dialogBuilder.setPositiveButton(getResources().getText(R.string.yes),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ moderateComment(CommentStatus.DELETE);
+ }
+ });
+ dialogBuilder.setNegativeButton(
+ getResources().getText(R.string.no),
+ null);
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+
+ } else {
+ moderateComment(CommentStatus.TRASH);
+ }
+
+ }
+ });
+
+ mBtnLikeComment.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ likeComment(false);
+ }
+ });
+
+ setupSuggestionServiceAndAdapter();
+
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ActivityId.trackLastActivity(ActivityId.COMMENT_DETAIL);
+
+ // Set the note if we retrieved the noteId from savedInstanceState
+ if (!TextUtils.isEmpty(mRestoredNoteId)) {
+ setNoteWithNoteId(mRestoredNoteId);
+ mRestoredNoteId = null;
+ }
+
+ if (mShouldLikeInstantly) {
+ mShouldLikeInstantly = false;
+ likeComment(true);
+ } else if (mShouldApproveInstantly) {
+ mShouldApproveInstantly = false;
+ performModerateAction();
+ }
+
+ }
+
+ private void setupSuggestionServiceAndAdapter() {
+ if (!isAdded()) return;
+
+ mSuggestionServiceConnectionManager = new SuggestionServiceConnectionManager(getActivity(), mRemoteBlogId);
+ mSuggestionAdapter = SuggestionUtils.setupSuggestions(mRemoteBlogId, getActivity(), mSuggestionServiceConnectionManager);
+ if (mSuggestionAdapter != null) {
+ mEditReply.setAdapter(mSuggestionAdapter);
+ }
+ }
+
+ private void setComment(int localBlogId, long commentId) {
+ setComment(localBlogId, CommentTable.getComment(localBlogId, commentId));
+ }
+
+ private void setComment(int localBlogId, final Comment comment) {
+ mComment = comment;
+ mLocalBlogId = localBlogId;
+
+ // is this comment on one of the user's blogs? it won't be if this was displayed from a
+ // notification about a reply to a comment this user posted on someone else's blog
+ mIsUsersBlog = (comment != null && WordPress.wpDB.isLocalBlogIdInDatabase(mLocalBlogId));
+
+ if (mIsUsersBlog)
+ mRemoteBlogId = WordPress.wpDB.getRemoteBlogIdForLocalTableBlogId(mLocalBlogId);
+
+ if (isAdded())
+ showComment();
+ }
+
+ private void disableShouldFocusReplyField() {
+ mShouldFocusReplyField = false;
+ }
+
+ private void enableShouldRequestCommentFromNote() {
+ mShouldRequestCommentFromNote = true;
+ }
+
+ @Override
+ public Note getNote() {
+ return mNote;
+ }
+
+ @Override
+ public void setNote(Note note) {
+ mNote = note;
+ if (isAdded() && mNote != null) {
+ showComment();
+ }
+ }
+
+ private void setNoteWithNoteId(String noteId) {
+ if (noteId == null) return;
+
+ if (SimperiumUtils.getNotesBucket() != null) {
+ try {
+ Note note = SimperiumUtils.getNotesBucket().get(noteId);
+ setNote(note);
+ setRemoteBlogId(note.getSiteId());
+ } catch (BucketObjectMissingException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation") // TODO: Remove when minSdkVersion >= 23
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (activity instanceof OnCommentChangeListener)
+ mOnCommentChangeListener = (OnCommentChangeListener) activity;
+ if (activity instanceof OnPostClickListener)
+ mOnPostClickListener = (OnPostClickListener) activity;
+ if (activity instanceof OnCommentActionListener)
+ mOnCommentActionListener = (OnCommentActionListener) activity;
+ if (activity instanceof OnNoteCommentActionListener)
+ mOnNoteCommentActionListener = (OnNoteCommentActionListener) activity;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ showComment();
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(SuggestionEvents.SuggestionNameListUpdated event) {
+ // check if the updated suggestions are for the current blog and update the suggestions
+ if (event.mRemoteBlogId != 0 && event.mRemoteBlogId == mRemoteBlogId && mSuggestionAdapter != null) {
+ List<Suggestion> suggestions = SuggestionTable.getSuggestionsForSite(event.mRemoteBlogId);
+ mSuggestionAdapter.setSuggestionList(suggestions);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ // Reset comment if this is from a notification
+ if (mNote != null) {
+ mComment = null;
+ }
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == Constants.INTENT_COMMENT_EDITOR && resultCode == Activity.RESULT_OK) {
+ if (mNote == null) {
+ reloadComment();
+ }
+ // tell the host to reload the comment list
+ if (mOnCommentChangeListener != null)
+ mOnCommentChangeListener.onCommentChanged(ChangeType.EDITED);
+ }
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ menu.clear();
+ inflater.inflate(R.menu.comment_detail, menu);
+ if (!canEdit()) {
+ menu.removeItem(R.id.menu_edit_comment);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == R.id.menu_edit_comment) {
+ editComment();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ private boolean hasComment() {
+ return (mComment != null);
+ }
+
+ private long getCommentId() {
+ return (mComment != null ? mComment.commentID : 0);
+ }
+
+ private int getLocalBlogId() {
+ return mLocalBlogId;
+ }
+
+ private int getRemoteBlogId() {
+ return mRemoteBlogId;
+ }
+
+ private void setRemoteBlogId(int remoteBlogId) {
+ mRemoteBlogId = remoteBlogId;
+ }
+
+ /*
+ * reload the current comment from the local database
+ */
+ private void reloadComment() {
+ if (!hasComment())
+ return;
+ Comment updatedComment = CommentTable.getComment(mLocalBlogId, getCommentId());
+ setComment(mLocalBlogId, updatedComment);
+ }
+
+ /*
+ * open the comment for editing
+ */
+ private void editComment() {
+ if (!isAdded() || !hasComment())
+ return;
+ // IMPORTANT: don't use getActivity().startActivityForResult() or else onActivityResult()
+ // won't be called in this fragment
+ // https://code.google.com/p/android/issues/detail?id=15394#c45
+ Intent intent = new Intent(getActivity(), EditCommentActivity.class);
+ intent.putExtra(EditCommentActivity.ARG_LOCAL_BLOG_ID, getLocalBlogId());
+ intent.putExtra(EditCommentActivity.ARG_COMMENT_ID, getCommentId());
+ if (mNote != null) {
+ intent.putExtra(EditCommentActivity.ARG_NOTE_ID, mNote.getId());
+ }
+ startActivityForResult(intent, Constants.INTENT_COMMENT_EDITOR);
+ }
+
+ /*
+ * display the current comment
+ */
+ private void showComment() {
+ if (!isAdded() || getView() == null)
+ return;
+
+ // these two views contain all the other views except the progress bar
+ final ScrollView scrollView = (ScrollView) getView().findViewById(R.id.scroll_view);
+ final View layoutBottom = getView().findViewById(R.id.layout_bottom);
+
+ // hide container views when comment is null (will happen when opened from a notification)
+ if (mComment == null) {
+ scrollView.setVisibility(View.GONE);
+ layoutBottom.setVisibility(View.GONE);
+
+ if (mNote != null && mShouldRequestCommentFromNote) {
+ // If a remote comment was requested, check if we have the comment for display.
+ // Otherwise request the comment via the REST API
+ int localTableBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(mNote.getSiteId());
+ if (localTableBlogId > 0) {
+ Comment comment = CommentTable.getComment(localTableBlogId, mNote.getParentCommentId());
+ if (comment != null) {
+ setComment(localTableBlogId, comment);
+ return;
+ }
+ }
+
+ long commentId = mNote.getParentCommentId() > 0 ? mNote.getParentCommentId() : mNote.getCommentId();
+ requestComment(localTableBlogId, mNote.getSiteId(), commentId);
+ } else if (mNote != null) {
+ showCommentForNote(mNote);
+ }
+
+ return;
+ }
+
+ scrollView.setVisibility(View.VISIBLE);
+ layoutBottom.setVisibility(View.VISIBLE);
+
+ // Add action buttons footer
+ if ((mNote == null || mShouldRequestCommentFromNote) && mLayoutButtons.getParent() == null) {
+ ViewGroup commentContentLayout = (ViewGroup) getView().findViewById(R.id.comment_content_container);
+ commentContentLayout.addView(mLayoutButtons);
+ }
+
+ final WPNetworkImageView imgAvatar = (WPNetworkImageView) getView().findViewById(R.id.image_avatar);
+ final TextView txtName = (TextView) getView().findViewById(R.id.text_name);
+ final TextView txtDate = (TextView) getView().findViewById(R.id.text_date);
+
+ txtName.setText(mComment.hasAuthorName() ? HtmlUtils.fastUnescapeHtml(mComment.getAuthorName()) : getString(R.string.anonymous));
+ txtDate.setText(DateTimeUtils.javaDateToTimeSpan(mComment.getDatePublished(), WordPress.getContext()));
+
+ int maxImageSz = getResources().getDimensionPixelSize(R.dimen.reader_comment_max_image_size);
+ CommentUtils.displayHtmlComment(mTxtContent, mComment.getCommentText(), maxImageSz);
+
+ int avatarSz = getResources().getDimensionPixelSize(R.dimen.avatar_sz_large);
+ if (mComment.hasProfileImageUrl()) {
+ imgAvatar.setImageUrl(GravatarUtils.fixGravatarUrl(mComment.getProfileImageUrl(), avatarSz), WPNetworkImageView.ImageType.AVATAR);
+ } else if (mComment.hasAuthorEmail()) {
+ String avatarUrl = GravatarUtils.gravatarFromEmail(mComment.getAuthorEmail(), avatarSz);
+ imgAvatar.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR);
+ } else {
+ imgAvatar.setImageUrl(null, WPNetworkImageView.ImageType.AVATAR);
+ }
+
+ updateStatusViews();
+
+ // navigate to author's blog when avatar or name clicked
+ if (mComment.hasAuthorUrl()) {
+ View.OnClickListener authorListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ReaderActivityLauncher.openUrl(getActivity(), mComment.getAuthorUrl());
+ }
+ };
+ imgAvatar.setOnClickListener(authorListener);
+ txtName.setOnClickListener(authorListener);
+ txtName.setTextColor(ContextCompat.getColor(getActivity(), R.color.reader_hyperlink));
+ } else {
+ txtName.setTextColor(ContextCompat.getColor(getActivity(), R.color.grey_darken_30));
+ }
+
+ showPostTitle(getRemoteBlogId(), mComment.postID);
+
+ // make sure reply box is showing
+ if (mLayoutReply.getVisibility() != View.VISIBLE && canReply()) {
+ AniUtils.animateBottomBar(mLayoutReply, true);
+ if (mEditReply != null && mShouldFocusReplyField) {
+ mEditReply.requestFocus();
+ disableShouldFocusReplyField();
+ }
+ }
+
+ getFragmentManager().invalidateOptionsMenu();
+ }
+
+ /*
+ * displays the passed post title for the current comment, updates stored title if one doesn't exist
+ */
+ private void setPostTitle(TextView txtTitle, String postTitle, boolean isHyperlink) {
+ if (txtTitle == null || !isAdded())
+ return;
+ if (TextUtils.isEmpty(postTitle)) {
+ txtTitle.setText(R.string.untitled);
+ return;
+ }
+
+ // if comment doesn't have a post title, set it to the passed one and save to comment table
+ if (hasComment() && !mComment.hasPostTitle()) {
+ mComment.setPostTitle(postTitle);
+ CommentTable.updateCommentPostTitle(getLocalBlogId(), getCommentId(), postTitle);
+ }
+
+ // display "on [Post Title]..."
+ if (isHyperlink) {
+ String html = getString(R.string.on)
+ + " <font color=" + HtmlUtils.colorResToHtmlColor(getActivity(), R.color.reader_hyperlink) + ">"
+ + postTitle.trim()
+ + "</font>";
+ txtTitle.setText(Html.fromHtml(html));
+ } else {
+ String text = getString(R.string.on) + " " + postTitle.trim();
+ txtTitle.setText(text);
+ }
+ }
+
+ /*
+ * ensure the post associated with this comment is available to the reader and show its
+ * title above the comment
+ */
+ private void showPostTitle(final int blogId, final long postId) {
+ if (!isAdded())
+ return;
+
+ final TextView txtPostTitle = (TextView) getView().findViewById(R.id.text_post_title);
+ boolean postExists = ReaderPostTable.postExists(blogId, postId);
+
+ // the post this comment is on can only be requested if this is a .com blog or a
+ // jetpack-enabled self-hosted blog, and we have valid .com credentials
+ boolean isDotComOrJetpack = WordPress.wpDB.isRemoteBlogIdDotComOrJetpack(mRemoteBlogId);
+ boolean canRequestPost = isDotComOrJetpack && AccountHelper.isSignedInWordPressDotCom();
+
+ final String title;
+ final boolean hasTitle;
+ if (mComment.hasPostTitle()) {
+ // use comment's stored post title if available
+ title = mComment.getPostTitle();
+ hasTitle = true;
+ } else if (postExists) {
+ // use title from post if available
+ title = ReaderPostTable.getPostTitle(blogId, postId);
+ hasTitle = !TextUtils.isEmpty(title);
+ } else {
+ title = null;
+ hasTitle = false;
+ }
+ if (hasTitle) {
+ setPostTitle(txtPostTitle, title, canRequestPost);
+ } else if (canRequestPost) {
+ txtPostTitle.setText(postExists ? R.string.untitled : R.string.loading);
+ }
+
+ // if this is a .com or jetpack blog, tapping the title shows the associated post
+ // in the reader
+ if (canRequestPost) {
+ // first make sure this post is available to the reader, and once it's retrieved set
+ // the title if it wasn't set above
+ if (!postExists) {
+ AppLog.d(T.COMMENTS, "comment detail > retrieving post");
+ ReaderPostActions.requestPost(blogId, postId, new ReaderActions.OnRequestListener() {
+ @Override
+ public void onSuccess() {
+ if (!isAdded()) return;
+
+ // update title if it wasn't set above
+ if (!hasTitle) {
+ String postTitle = ReaderPostTable.getPostTitle(blogId, postId);
+ if (!TextUtils.isEmpty(postTitle)) {
+ setPostTitle(txtPostTitle, postTitle, true);
+ } else {
+ txtPostTitle.setText(R.string.untitled);
+ }
+ }
+ }
+
+ @Override
+ public void onFailure(int statusCode) {
+ }
+ });
+ }
+
+ txtPostTitle.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mOnPostClickListener != null) {
+ mOnPostClickListener.onPostClicked(getNote(), mRemoteBlogId, (int) mComment.postID);
+ } else {
+ // right now this will happen from notifications
+ AppLog.i(T.COMMENTS, "comment detail > no post click listener");
+ ReaderActivityLauncher.showReaderPostDetail(getActivity(), mRemoteBlogId, mComment.postID);
+ }
+ }
+ });
+ }
+ }
+
+ private void trackModerationFromNotification(final CommentStatus newStatus) {
+ switch (newStatus) {
+ case APPROVED:
+ AnalyticsTracker.track(Stat.NOTIFICATION_APPROVED);
+ break;
+ case UNAPPROVED:
+ AnalyticsTracker.track(Stat.NOTIFICATION_UNAPPROVED);
+ break;
+ case SPAM:
+ AnalyticsTracker.track(Stat.NOTIFICATION_FLAGGED_AS_SPAM);
+ break;
+ case TRASH:
+ AnalyticsTracker.track(Stat.NOTIFICATION_TRASHED);
+ break;
+ }
+ }
+
+ /*
+ * approve, disapprove, spam, or trash the current comment
+ */
+ private void moderateComment(final CommentStatus newStatus) {
+ if (!isAdded() || !hasComment())
+ return;
+ if (!NetworkUtils.checkConnection(getActivity()))
+ return;
+
+ // Fire the appropriate listener if we have one
+ if (mNote != null && mOnNoteCommentActionListener != null) {
+ mOnNoteCommentActionListener.onModerateCommentForNote(mNote, newStatus);
+ trackModerationFromNotification(newStatus);
+ return;
+ } else if (mOnCommentActionListener != null) {
+ mOnCommentActionListener.onModerateComment(mLocalBlogId, mComment, newStatus);
+ return;
+ }
+
+ if (mNote == null) return;
+
+ // Basic moderation support, currently only used when this Fragment is in a CommentDetailActivity
+ // Uses WP.com REST API and requires a note object
+ final CommentStatus oldStatus = mComment.getStatusEnum();
+ mComment.setStatus(CommentStatus.toString(newStatus));
+ updateStatusViews();
+ CommentActions.moderateCommentRestApi(mNote.getSiteId(), mComment.commentID, newStatus, new CommentActions.CommentActionListener() {
+ @Override
+ public void onActionResult(CommentActionResult result) {
+ if (!isAdded()) return;
+
+ if (result.isSuccess()) {
+ ToastUtils.showToast(getActivity(), R.string.comment_moderated_approved, ToastUtils.Duration.SHORT);
+ } else {
+ mComment.setStatus(CommentStatus.toString(oldStatus));
+ updateStatusViews();
+ ToastUtils.showToast(getActivity(), R.string.error_moderate_comment);
+ }
+ }
+ });
+ }
+
+ /*
+ * post comment box text as a reply to the current comment
+ */
+ private void submitReply() {
+ if (!hasComment() || !isAdded() || mIsSubmittingReply)
+ return;
+
+ if (!NetworkUtils.checkConnection(getActivity()))
+ return;
+
+ final String replyText = EditTextUtils.getText(mEditReply);
+ if (TextUtils.isEmpty(replyText))
+ return;
+
+ // disable editor, hide soft keyboard, hide submit icon, and show progress spinner while submitting
+ mEditReply.setEnabled(false);
+ EditTextUtils.hideSoftInput(mEditReply);
+ mSubmitReplyBtn.setVisibility(View.GONE);
+ final ProgressBar progress = (ProgressBar) getView().findViewById(R.id.progress_submit_comment);
+ progress.setVisibility(View.VISIBLE);
+
+ CommentActions.CommentActionListener actionListener = new CommentActions.CommentActionListener() {
+ @Override
+ public void onActionResult(CommentActionResult result) {
+ mIsSubmittingReply = false;
+ if (result.isSuccess() && mOnCommentChangeListener != null)
+ mOnCommentChangeListener.onCommentChanged(ChangeType.REPLIED);
+ if (isAdded()) {
+ mEditReply.setEnabled(true);
+ mSubmitReplyBtn.setVisibility(View.VISIBLE);
+ progress.setVisibility(View.GONE);
+ updateStatusViews();
+ if (result.isSuccess()) {
+ ToastUtils.showToast(getActivity(), getString(R.string.note_reply_successful));
+ mEditReply.setText(null);
+ mEditReply.getAutoSaveTextHelper().clearSavedText(mEditReply);
+
+ // approve the comment
+ if (mComment != null && mComment.getStatusEnum() != CommentStatus.APPROVED) {
+ moderateComment(CommentStatus.APPROVED);
+ }
+ } else {
+ String errorMessage = TextUtils.isEmpty(result.getMessage()) ? getString(R.string.reply_failed) : result.getMessage();
+ String strUnEscapeHTML = StringEscapeUtils.unescapeHtml(errorMessage);
+ ToastUtils.showToast(getActivity(), strUnEscapeHTML, ToastUtils.Duration.LONG);
+ // refocus editor on failure and show soft keyboard
+ EditTextUtils.showSoftInput(mEditReply);
+ }
+ }
+ }
+ };
+
+ mIsSubmittingReply = true;
+
+ AnalyticsTracker.track(AnalyticsTracker.Stat.NOTIFICATION_REPLIED_TO);
+ if (mNote != null) {
+ if (mShouldRequestCommentFromNote) {
+ CommentActions.submitReplyToCommentRestApi(mNote.getSiteId(), mComment.commentID, replyText, actionListener);
+ } else {
+ CommentActions.submitReplyToCommentNote(mNote, replyText, actionListener);
+ }
+ } else {
+ CommentActions.submitReplyToComment(mLocalBlogId, mComment, replyText, actionListener);
+ }
+ }
+
+ /*
+ * sets the drawable for moderation buttons
+ */
+ private void setTextDrawable(final TextView view, int resId) {
+ view.setCompoundDrawablesWithIntrinsicBounds(null, ContextCompat.getDrawable(getActivity(), resId), null, null);
+ }
+
+ /*
+ * update the text, drawable & click listener for mBtnModerate based on
+ * the current status of the comment, show mBtnSpam if the comment isn't
+ * already marked as spam, and show the current status of the comment
+ */
+ private void updateStatusViews() {
+ if (!isAdded() || !hasComment())
+ return;
+
+ final int statusTextResId; // string resource id for status text
+ final int statusColor; // color for status text
+
+ switch (mComment.getStatusEnum()) {
+ case APPROVED:
+ statusTextResId = R.string.comment_status_approved;
+ statusColor = ContextCompat.getColor(getActivity(), R.color.notification_status_unapproved_dark);
+ break;
+ case UNAPPROVED:
+ statusTextResId = R.string.comment_status_unapproved;
+ statusColor = ContextCompat.getColor(getActivity(), R.color.notification_status_unapproved_dark);
+ break;
+ case SPAM:
+ statusTextResId = R.string.comment_status_spam;
+ statusColor = ContextCompat.getColor(getActivity(), R.color.comment_status_spam);
+ break;
+ case TRASH:
+ default:
+ statusTextResId = R.string.comment_status_trash;
+ statusColor = ContextCompat.getColor(getActivity(), R.color.comment_status_spam);
+ break;
+ }
+
+ if (mNote != null && canLike()) {
+ mBtnLikeComment.setVisibility(View.VISIBLE);
+
+ toggleLikeButton(mNote.hasLikedComment());
+ } else {
+ mBtnLikeComment.setVisibility(View.GONE);
+ }
+
+ // comment status is only shown if this comment is from one of this user's blogs and the
+ // comment hasn't been approved
+ if (mIsUsersBlog && mComment.getStatusEnum() != CommentStatus.APPROVED) {
+ mTxtStatus.setText(getString(statusTextResId).toUpperCase());
+ mTxtStatus.setTextColor(statusColor);
+ if (mTxtStatus.getVisibility() != View.VISIBLE) {
+ mTxtStatus.clearAnimation();
+ AniUtils.fadeIn(mTxtStatus, AniUtils.Duration.LONG);
+ }
+ } else {
+ mTxtStatus.setVisibility(View.GONE);
+ }
+
+ if (canModerate()) {
+ setModerateButtonForStatus(mComment.getStatusEnum());
+ mBtnModerateComment.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ performModerateAction();
+ }
+ });
+ mBtnModerateComment.setVisibility(View.VISIBLE);
+ } else {
+ mBtnModerateComment.setVisibility(View.GONE);
+ }
+
+ if (canMarkAsSpam()) {
+ mBtnSpamComment.setVisibility(View.VISIBLE);
+ if (mComment.getStatusEnum() == CommentStatus.SPAM) {
+ mBtnSpamComment.setText(R.string.mnu_comment_unspam);
+ } else {
+ mBtnSpamComment.setText(R.string.mnu_comment_spam);
+ }
+ } else {
+ mBtnSpamComment.setVisibility(View.GONE);
+ }
+
+ if (canTrash()) {
+ mBtnTrashComment.setVisibility(View.VISIBLE);
+ if (mComment.getStatusEnum() == CommentStatus.TRASH) {
+ mBtnModerateIcon.setImageResource(R.drawable.ic_action_restore);
+ //mBtnModerateTextView.setTextColor(getActivity().getResources().getColor(R.color.notification_status_unapproved_dark));
+ mBtnModerateTextView.setText(R.string.mnu_comment_untrash);
+ mBtnTrashComment.setText(R.string.mnu_comment_delete_permanently);
+ } else {
+ mBtnTrashComment.setText(R.string.mnu_comment_trash);
+ }
+ } else {
+ mBtnTrashComment.setVisibility(View.GONE);
+ }
+
+ mLayoutButtons.setVisibility(View.VISIBLE);
+ }
+
+ private void performModerateAction(){
+ if (!hasComment() || !isAdded() || !NetworkUtils.checkConnection(getActivity())) {
+ return;
+ }
+
+ CommentStatus newStatus = CommentStatus.APPROVED;
+ if (mComment.getStatusEnum() == CommentStatus.APPROVED) {
+ newStatus = CommentStatus.UNAPPROVED;
+ }
+
+ mComment.setStatus(newStatus.toString());
+ setModerateButtonForStatus(newStatus);
+ AniUtils.startAnimation(mBtnModerateIcon, R.anim.notifications_button_scale);
+ moderateComment(newStatus);
+ }
+
+ private void setModerateButtonForStatus(CommentStatus status) {
+ if (status == CommentStatus.APPROVED) {
+ mBtnModerateIcon.setImageResource(R.drawable.ic_action_approve_active);
+ mBtnModerateTextView.setText(R.string.comment_status_approved);
+ mBtnModerateTextView.setTextColor(ContextCompat.getColor(getActivity(), R.color.notification_status_unapproved_dark));
+ } else {
+ mBtnModerateIcon.setImageResource(R.drawable.ic_action_approve);
+ mBtnModerateTextView.setText(R.string.mnu_comment_approve);
+ mBtnModerateTextView.setTextColor(ContextCompat.getColor(getActivity(), R.color.grey));
+ }
+ }
+
+ /*
+ * does user have permission to moderate/reply/spam this comment?
+ */
+ private boolean canModerate() {
+ return mEnabledActions != null && (mEnabledActions.contains(EnabledActions.ACTION_APPROVE) || mEnabledActions.contains(EnabledActions.ACTION_UNAPPROVE));
+ }
+
+ private boolean canMarkAsSpam() {
+ return (mEnabledActions != null && mEnabledActions.contains(EnabledActions.ACTION_SPAM));
+ }
+
+ private boolean canReply() {
+ return (mEnabledActions != null && mEnabledActions.contains(EnabledActions.ACTION_REPLY));
+ }
+
+ private boolean canTrash() {
+ return canModerate();
+ }
+
+ private boolean canEdit() {
+ return (mLocalBlogId > 0 && canModerate());
+ }
+
+ private boolean canLike() {
+ return (!mShouldRequestCommentFromNote && mEnabledActions != null && mEnabledActions.contains(EnabledActions.ACTION_LIKE));
+ }
+
+ /*
+ * display the comment associated with the passed notification
+ */
+ private void showCommentForNote(Note note) {
+ if (getView() == null) return;
+ View view = getView();
+
+ // hide standard comment views, since we'll be adding note blocks instead
+ View commentContent = view.findViewById(R.id.comment_content);
+ if (commentContent != null) {
+ commentContent.setVisibility(View.GONE);
+ }
+
+ View commentText = view.findViewById(R.id.text_content);
+ if (commentText != null) {
+ commentText.setVisibility(View.GONE);
+ }
+
+ // Now we'll add a detail fragment list
+ FragmentManager fragmentManager = getFragmentManager();
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+ mNotificationsDetailListFragment = NotificationsDetailListFragment.newInstance(note.getId());
+ mNotificationsDetailListFragment.setFooterView(mLayoutButtons);
+ // Listen for note changes from the detail list fragment, so we can update the status buttons
+ mNotificationsDetailListFragment.setOnNoteChangeListener(new NotificationsDetailListFragment.OnNoteChangeListener() {
+ @Override
+ public void onNoteChanged(Note note) {
+ mNote = note;
+ mComment = mNote.buildComment();
+ updateStatusViews();
+ }
+ });
+ fragmentTransaction.replace(R.id.comment_content_container, mNotificationsDetailListFragment);
+ fragmentTransaction.commitAllowingStateLoss();
+
+ /*
+ * determine which actions to enable for this comment - if the comment is from this user's
+ * blog then all actions will be enabled, but they won't be if it's a reply to a comment
+ * this user made on someone else's blog
+ */
+ mEnabledActions = note.getEnabledActions();
+
+ // Set 'Reply to (Name)' in comment reply EditText if it's a reasonable size
+ if (!TextUtils.isEmpty(mNote.getCommentAuthorName()) && mNote.getCommentAuthorName().length() < 28) {
+ mEditReply.setHint(String.format(getString(R.string.comment_reply_to_user), mNote.getCommentAuthorName()));
+ }
+
+ // note that the local blog id won't be found if the comment is from someone else's blog
+ int localBlogId = WordPress.wpDB.getLocalTableBlogIdForRemoteBlogId(mRemoteBlogId);
+
+ setComment(localBlogId, note.buildComment());
+
+ getFragmentManager().invalidateOptionsMenu();
+ }
+
+ private void setLikeCommentWhenReady() {
+ mShouldLikeInstantly = true;
+ }
+
+ private void setApproveCommentWhenReady() {
+ mShouldApproveInstantly = true;
+ }
+
+ // Like or unlike a comment via the REST API
+ private void likeComment(boolean forceLike) {
+ if (mNote == null) return;
+ if (!isAdded()) return;
+ if (forceLike && mBtnLikeComment.isActivated()) return;
+
+ toggleLikeButton(!mBtnLikeComment.isActivated());
+
+ ReaderAnim.animateLikeButton(mBtnLikeIcon, mBtnLikeComment.isActivated());
+
+ // Bump analytics
+ AnalyticsTracker.track(mBtnLikeComment.isActivated() ? Stat.NOTIFICATION_LIKED : Stat.NOTIFICATION_UNLIKED);
+
+ boolean commentWasUnapproved = false;
+ if (mNotificationsDetailListFragment != null && mComment != null) {
+ // Optimistically set comment to approved when liking an unapproved comment
+ // WP.com will set a comment to approved if it is liked while unapproved
+ if (mBtnLikeComment.isActivated() && mComment.getStatusEnum() == CommentStatus.UNAPPROVED) {
+ mComment.setStatus(CommentStatus.toString(CommentStatus.APPROVED));
+ mNotificationsDetailListFragment.refreshBlocksForCommentStatus(CommentStatus.APPROVED);
+ setModerateButtonForStatus(CommentStatus.APPROVED);
+ commentWasUnapproved = true;
+ }
+ }
+
+ final boolean commentStatusShouldRevert = commentWasUnapproved;
+ WordPress.getRestClientUtils().likeComment(String.valueOf(mNote.getSiteId()),
+ String.valueOf(mNote.getCommentId()),
+ mBtnLikeComment.isActivated(),
+ new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (response != null && !response.optBoolean("success")) {
+ if (!isAdded()) return;
+
+ // Failed, so switch the button state back
+ toggleLikeButton(!mBtnLikeComment.isActivated());
+
+ if (commentStatusShouldRevert) {
+ setCommentStatusUnapproved();
+ }
+ }
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (!isAdded()) return;
+
+ toggleLikeButton(!mBtnLikeComment.isActivated());
+
+ if (commentStatusShouldRevert) {
+ setCommentStatusUnapproved();
+ }
+ }
+ });
+ }
+
+ private void setCommentStatusUnapproved() {
+ mComment.setStatus(CommentStatus.toString(CommentStatus.UNAPPROVED));
+ mNotificationsDetailListFragment.refreshBlocksForCommentStatus(CommentStatus.UNAPPROVED);
+ setModerateButtonForStatus(CommentStatus.UNAPPROVED);
+ }
+
+ private void toggleLikeButton(boolean isLiked) {
+ if (isLiked) {
+ mBtnLikeTextView.setText(getResources().getString(R.string.mnu_comment_liked));
+ mBtnLikeTextView.setTextColor(ContextCompat.getColor(getActivity(), R.color.orange_jazzy));
+ mBtnLikeIcon.setImageDrawable(ContextCompat.getDrawable(getActivity(), R.drawable.ic_action_like_active));
+ mBtnLikeComment.setActivated(true);
+ } else {
+ mBtnLikeTextView.setText(getResources().getString(R.string.reader_label_like));
+ mBtnLikeTextView.setTextColor(ContextCompat.getColor(getActivity(), R.color.grey));
+ mBtnLikeIcon.setImageDrawable(ContextCompat.getDrawable(getActivity(), R.drawable.ic_action_like));
+ mBtnLikeComment.setActivated(false);
+ }
+ }
+
+ /*
+ * request a comment - note that this uses the REST API rather than XMLRPC, which means the user must
+ * either be wp.com or have Jetpack, but it's safe to do this since this method is only called when
+ * displayed from a notification (and notifications require wp.com/Jetpack)
+ */
+ private void requestComment(final int localBlogId,
+ final int remoteBlogId,
+ final long commentId) {
+
+ final ProgressBar progress = (isAdded() && getView() != null ?
+ (ProgressBar) getView().findViewById(R.id.progress_loading) : null);
+ if (progress != null) {
+ progress.setVisibility(View.VISIBLE);
+ }
+
+ RestRequest.Listener restListener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (isAdded()) {
+ if (progress != null) {
+ progress.setVisibility(View.GONE);
+ }
+ Comment comment = Comment.fromJSON(jsonObject);
+ if (comment != null) {
+ // save comment to local db if localBlogId is valid
+ if (localBlogId > 0) {
+ CommentTable.addComment(localBlogId, comment);
+ }
+ // now, at long last, show the comment
+ setComment(localBlogId, comment);
+ }
+ }
+ }
+ };
+ RestRequest.ErrorListener restErrListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(T.COMMENTS, VolleyUtils.errStringFromVolleyError(volleyError), volleyError);
+ if (isAdded()) {
+ if (progress != null) {
+ progress.setVisibility(View.GONE);
+ }
+ ToastUtils.showToast(getActivity(), R.string.reader_toast_err_get_comment, ToastUtils.Duration.LONG);
+ }
+ }
+ };
+
+ final String path = String.format("/sites/%s/comments/%s", remoteBlogId, commentId);
+ WordPress.getRestClientUtils().get(path, restListener, restErrListener);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDialogs.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDialogs.java
new file mode 100644
index 000000000..b2ec720ed
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDialogs.java
@@ -0,0 +1,52 @@
+package org.wordpress.android.ui.comments;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+
+import org.wordpress.android.R;
+
+/**
+ * Dialogs related to comment moderation displayed from CommentsActivity and NotificationsActivity
+ */
+class CommentDialogs {
+ public static final int ID_COMMENT_DLG_APPROVING = 100;
+ public static final int ID_COMMENT_DLG_DISAPPROVING = 101;
+ public static final int ID_COMMENT_DLG_SPAMMING = 102;
+ public static final int ID_COMMENT_DLG_TRASHING = 103;
+ public static final int ID_COMMENT_DLG_DELETING = 104;
+
+ private CommentDialogs() {
+ throw new AssertionError();
+ }
+
+ public static Dialog createCommentDialog(Activity activity, int dialogId) {
+ final int resId;
+ switch (dialogId) {
+ case ID_COMMENT_DLG_APPROVING :
+ resId = R.string.dlg_approving_comments;
+ break;
+ case ID_COMMENT_DLG_DISAPPROVING:
+ resId = R.string.dlg_unapproving_comments;
+ break;
+ case ID_COMMENT_DLG_TRASHING:
+ resId = R.string.dlg_trashing_comments;
+ break;
+ case ID_COMMENT_DLG_SPAMMING:
+ resId = R.string.dlg_spamming_comments;
+ break;
+ case ID_COMMENT_DLG_DELETING:
+ resId = R.string.dlg_deleting_comments;
+ break;
+ default :
+ return null;
+ }
+
+ ProgressDialog dialog = new ProgressDialog(activity);
+ dialog.setMessage(activity.getString(resId));
+ dialog.setIndeterminate(true);
+ dialog.setCancelable(false);
+
+ return dialog;
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentEvents.java
new file mode 100644
index 000000000..88a82fd7f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentEvents.java
@@ -0,0 +1,56 @@
+package org.wordpress.android.ui.comments;
+
+import org.wordpress.android.models.CommentList;
+import org.wordpress.android.models.CommentStatus;
+
+class CommentEvents {
+
+ public static class CommentsBatchModerationFinishedEvent {
+ private final CommentList mComments;
+ private final boolean mIsDeleted;
+
+ public CommentsBatchModerationFinishedEvent(CommentList comments, boolean isDeleted) {
+ mComments = comments;
+ mIsDeleted = isDeleted;
+ }
+
+ public CommentList getComments() {
+ return mComments;
+ }
+
+ public boolean isDeleted() {
+ return mIsDeleted;
+ }
+
+ }
+
+ public static class CommentModerationFinishedEvent {
+ private final boolean mIsSuccess;
+ private final boolean mIsCommentsRefreshRequired;
+ private final long mCommentId;
+ private final CommentStatus mNewStatus;
+
+ public CommentModerationFinishedEvent(boolean isSuccess, boolean isCommentsRefreshRequired, long commentId, CommentStatus newStatus) {
+ mIsSuccess = isSuccess;
+ mIsCommentsRefreshRequired = isCommentsRefreshRequired;
+ mCommentId = commentId;
+ mNewStatus = newStatus;
+ }
+
+ public boolean isSuccess() {
+ return mIsSuccess;
+ }
+
+ public boolean isCommentsRefreshRequired() {
+ return mIsCommentsRefreshRequired;
+ }
+
+ public long getCommentId() {
+ return mCommentId;
+ }
+
+ public CommentStatus getNewStatus() {
+ return mNewStatus;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentUtils.java
new file mode 100644
index 000000000..d21d5231f
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentUtils.java
@@ -0,0 +1,107 @@
+package org.wordpress.android.ui.comments;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.support.v4.content.ContextCompat;
+import android.text.Layout;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.style.LeadingMarginSpan;
+import android.text.util.Linkify;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.util.EmoticonsUtils;
+import org.wordpress.android.util.HtmlUtils;
+import org.wordpress.android.util.helpers.WPImageGetter;
+
+public class CommentUtils {
+ /*
+ * displays comment text as html, including retrieving images
+ */
+ public static void displayHtmlComment(TextView textView, String content, int maxImageSize) {
+ if (textView == null) {
+ return;
+ }
+
+ if (content == null) {
+ textView.setText(null);
+ return;
+ }
+
+ // skip performance hit of html conversion if content doesn't contain html
+ if (!content.contains("<") && !content.contains("&")) {
+ content = content.trim();
+ textView.setText(content);
+ // make sure unnamed links are clickable
+ if (content.contains("://")) {
+ Linkify.addLinks(textView, Linkify.WEB_URLS);
+ }
+ return;
+ }
+
+ // convert emoticons first (otherwise they'll be downloaded)
+ content = EmoticonsUtils.replaceEmoticonsWithEmoji(content);
+
+ // now convert to HTML with an image getter that enforces a max image size
+ final Spanned html;
+ if (maxImageSize > 0 && content.contains("<img")) {
+ Drawable loading = ContextCompat.getDrawable(textView.getContext(),
+ R.drawable.legacy_dashicon_format_image_big_grey);
+ Drawable failed = ContextCompat.getDrawable(textView.getContext(),
+ R.drawable.noticon_warning_big_grey);
+ html = HtmlUtils.fromHtml(content, new WPImageGetter(textView, maxImageSize, WordPress.imageLoader, loading,
+ failed));
+ } else {
+ html = HtmlUtils.fromHtml(content);
+ }
+
+ // remove extra \n\n added by Html.convert()
+ int start = 0;
+ int end = html.length();
+ while (start < end && Character.isWhitespace(html.charAt(start))) {
+ start++;
+ }
+ while (end > start && Character.isWhitespace(html.charAt(end - 1))) {
+ end--;
+ }
+
+ textView.setText(html.subSequence(start, end));
+ }
+
+ // Assumes all lines after first line will not be indented
+ public static void indentTextViewFirstLine(TextView textView, int textOffsetX) {
+ if (textView == null || textOffsetX < 0) return;
+
+ SpannableString text = new SpannableString(textView.getText());
+ text.setSpan(new TextWrappingLeadingMarginSpan(textOffsetX), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ textView.setText(text);
+ }
+
+ private static class TextWrappingLeadingMarginSpan implements LeadingMarginSpan.LeadingMarginSpan2 {
+ private final int margin;
+ private final int lines;
+
+ public TextWrappingLeadingMarginSpan(int margin) {
+ this.margin = margin;
+ this.lines = 1;
+ }
+
+ @Override
+ public int getLeadingMargin(boolean first) {
+ return first ? margin : 0;
+ }
+
+ @Override
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
+
+ }
+
+ @Override
+ public int getLeadingMarginLineCount() {
+ return lines;
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsActivity.java
new file mode 100644
index 000000000..e82645fe6
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsActivity.java
@@ -0,0 +1,314 @@
+package org.wordpress.android.ui.comments;
+
+import android.app.Dialog;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.Snackbar;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuItem;
+import android.view.View;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentList;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.ActivityLauncher;
+import org.wordpress.android.ui.comments.CommentsListFragment.OnCommentSelectedListener;
+import org.wordpress.android.ui.notifications.NotificationFragment;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.ui.reader.ReaderPostDetailFragment;
+import org.wordpress.android.util.AppLog;
+
+import de.greenrobot.event.EventBus;
+
+public class CommentsActivity extends AppCompatActivity
+ implements OnCommentSelectedListener,
+ NotificationFragment.OnPostClickListener,
+ CommentActions.OnCommentActionListener,
+ CommentActions.OnCommentChangeListener {
+ private static final String KEY_SELECTED_COMMENT_ID = "selected_comment_id";
+ static final String KEY_AUTO_REFRESHED = "has_auto_refreshed";
+ static final String KEY_EMPTY_VIEW_MESSAGE = "empty_view_message";
+ private static final String SAVED_COMMENTS_STATUS_TYPE = "saved_comments_status_type";
+ private long mSelectedCommentId;
+ private final CommentList mTrashedComments = new CommentList();
+
+ private CommentStatus mCurrentCommentStatusType = CommentStatus.UNKNOWN;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.comment_activity);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setElevation(0);
+ actionBar.setTitle(R.string.comments);
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ if (getIntent() != null && getIntent().hasExtra(SAVED_COMMENTS_STATUS_TYPE)) {
+ mCurrentCommentStatusType = (CommentStatus) getIntent().getSerializableExtra(SAVED_COMMENTS_STATUS_TYPE);
+ } else {
+ // Read the value from app preferences here. Default to 0 - All
+ mCurrentCommentStatusType = AppPrefs.getCommentsStatusFilter();
+ }
+
+ if (savedInstanceState == null) {
+ CommentsListFragment commentsListFragment = new CommentsListFragment();
+ // initialize comment status filter first time
+ commentsListFragment.setCommentStatusFilter(mCurrentCommentStatusType);
+ getFragmentManager().beginTransaction()
+ .add(R.id.layout_fragment_container, commentsListFragment, getString(R.string
+ .fragment_tag_comment_list))
+ .commitAllowingStateLoss();
+ } else {
+ getIntent().putExtra(KEY_AUTO_REFRESHED, savedInstanceState.getBoolean(KEY_AUTO_REFRESHED));
+ getIntent().putExtra(KEY_EMPTY_VIEW_MESSAGE, savedInstanceState.getString(KEY_EMPTY_VIEW_MESSAGE));
+
+ mSelectedCommentId = savedInstanceState.getLong(KEY_SELECTED_COMMENT_ID);
+ }
+
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ ActivityId.trackLastActivity(ActivityId.COMMENTS);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (getFragmentManager().getBackStackEntryCount() > 0) {
+ getFragmentManager().popBackStack();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ AppLog.d(AppLog.T.COMMENTS, "comment activity new intent");
+ }
+
+
+ private CommentDetailFragment getDetailFragment() {
+ Fragment fragment = getFragmentManager().findFragmentByTag(getString(
+ R.string.fragment_tag_comment_detail));
+ if (fragment == null) {
+ return null;
+ }
+ return (CommentDetailFragment) fragment;
+ }
+
+ private boolean hasDetailFragment() {
+ return (getDetailFragment() != null);
+ }
+
+ private CommentsListFragment getListFragment() {
+ Fragment fragment = getFragmentManager().findFragmentByTag(getString(
+ R.string.fragment_tag_comment_list));
+ if (fragment == null) {
+ return null;
+ }
+ return (CommentsListFragment) fragment;
+ }
+
+ private boolean hasListFragment() {
+ return (getListFragment() != null);
+ }
+
+ private void showReaderFragment(long remoteBlogId, long postId) {
+ FragmentManager fm = getFragmentManager();
+ fm.executePendingTransactions();
+
+ Fragment fragment = ReaderPostDetailFragment.newInstance(remoteBlogId, postId);
+ FragmentTransaction ft = fm.beginTransaction();
+ String tagForFragment = getString(R.string.fragment_tag_reader_post_detail);
+ ft.add(R.id.layout_fragment_container, fragment, tagForFragment)
+ .addToBackStack(tagForFragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
+ if (hasDetailFragment())
+ ft.hide(getDetailFragment());
+ ft.commit();
+ }
+
+ /*
+ * called from comment list when user taps a comment
+ */
+ @Override
+ public void onCommentSelected(long commentId) {
+ mSelectedCommentId = commentId;
+ FragmentManager fm = getFragmentManager();
+ if (fm == null) return;
+
+ fm.executePendingTransactions();
+ CommentsListFragment listFragment = getListFragment();
+
+ FragmentTransaction ft = fm.beginTransaction();
+ String tagForFragment = getString(R.string.fragment_tag_comment_detail);
+ CommentDetailFragment detailFragment = CommentDetailFragment.newInstance(WordPress.getCurrentLocalTableBlogId(),
+ commentId);
+ ft.add(R.id.layout_fragment_container, detailFragment, tagForFragment).addToBackStack(tagForFragment)
+ .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
+ if (listFragment != null) {
+ ft.hide(listFragment);
+ }
+ ft.commitAllowingStateLoss();
+ }
+
+ /*
+ * called from comment detail when user taps a link to a post - show the post in a
+ * reader detail fragment
+ */
+ @Override
+ public void onPostClicked(Note note, int remoteBlogId, int postId) {
+ showReaderFragment(remoteBlogId, postId);
+ }
+
+ /*
+ * reload the comment list from existing data
+ */
+ private void reloadCommentList() {
+ CommentsListFragment listFragment = getListFragment();
+ if (listFragment != null)
+ listFragment.loadComments();
+ }
+
+ /*
+ * tell the comment list to get recent comments from server
+ */
+ private void updateCommentList() {
+ CommentsListFragment listFragment = getListFragment();
+ if (listFragment != null) {
+ //listFragment.setRefreshing(true);
+ listFragment.setCommentStatusFilter(mCurrentCommentStatusType);
+ listFragment.updateComments(false);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ // https://code.google.com/p/android/issues/detail?id=19917
+ if (outState.isEmpty()) {
+ outState.putBoolean("bug_19917_fix", true);
+ }
+
+ // retain the id of the highlighted and selected comments
+ if (mSelectedCommentId != 0 && hasDetailFragment()) {
+ outState.putLong(KEY_SELECTED_COMMENT_ID, mSelectedCommentId);
+ }
+
+ if (hasListFragment()) {
+ outState.putBoolean(KEY_AUTO_REFRESHED, getListFragment().mHasAutoRefreshedComments);
+ outState.putString(KEY_EMPTY_VIEW_MESSAGE, getListFragment().getEmptyViewMessage());
+ }
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ Dialog dialog = CommentDialogs.createCommentDialog(this, id);
+ if (dialog != null)
+ return dialog;
+ return super.onCreateDialog(id);
+ }
+
+ @Override
+ public void onModerateComment(final int accountId, final Comment comment, final CommentStatus newStatus) {
+ FragmentManager fm = getFragmentManager();
+ if (fm.getBackStackEntryCount() > 0) {
+ fm.popBackStack();
+ }
+
+ if (newStatus == CommentStatus.APPROVED || newStatus == CommentStatus.UNAPPROVED) {
+ getListFragment().setCommentIsModerating(comment.commentID, true);
+ getListFragment().updateEmptyView();
+ CommentActions.moderateComment(accountId, comment, newStatus,
+ new CommentActions.CommentActionListener() {
+ @Override
+ public void onActionResult(CommentActionResult result) {
+ EventBus.getDefault().post(new CommentEvents.CommentModerationFinishedEvent
+ (result.isSuccess(), true, comment.commentID, newStatus));
+ }
+ });
+ } else if (newStatus == CommentStatus.SPAM || newStatus == CommentStatus.TRASH || newStatus == CommentStatus.DELETE) {
+ mTrashedComments.add(comment);
+ getListFragment().removeComment(comment);
+ getListFragment().setCommentIsModerating(comment.commentID, true);
+ getListFragment().updateEmptyView();
+
+ String message = (newStatus == CommentStatus.TRASH ? getString(R.string.comment_trashed) : newStatus == CommentStatus.SPAM ? getString(R.string.comment_spammed) : getString(R.string.comment_deleted_permanently));
+ View.OnClickListener undoListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mTrashedComments.remove(comment);
+ getListFragment().setCommentIsModerating(comment.commentID, false);
+ getListFragment().loadComments();
+ }
+ };
+
+ Snackbar snackbar = Snackbar.make(getListFragment().getView(), message, Snackbar.LENGTH_LONG)
+ .setAction(R.string.undo, undoListener);
+
+ // do the actual moderation once the undo bar has been hidden
+ snackbar.setCallback(new Snackbar.Callback() {
+ @Override
+ public void onDismissed(Snackbar snackbar, int event) {
+ super.onDismissed(snackbar, event);
+
+ // comment will no longer exist in moderating list if action was undone
+ if (!mTrashedComments.contains(comment)) {
+ return;
+ }
+ mTrashedComments.remove(comment);
+ CommentActions.moderateComment(accountId, comment, newStatus, new CommentActions.CommentActionListener() {
+ @Override
+ public void onActionResult(CommentActionResult result) {
+ EventBus.getDefault().post(new CommentEvents.CommentModerationFinishedEvent
+ (result.isSuccess(), true, comment.commentID, newStatus));
+ }
+ });
+ }
+ });
+
+ snackbar.show();
+ }
+ }
+
+ @Override
+ public void onCommentChanged(CommentActions.ChangeType changeType) {
+ switch (changeType) {
+ case EDITED:
+ reloadCommentList();
+ break;
+ case REPLIED:
+ updateCommentList();
+ break;
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsListFragment.java
new file mode 100644
index 000000000..1b61519cc
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentsListFragment.java
@@ -0,0 +1,795 @@
+package org.wordpress.android.ui.comments;
+
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.view.ActionMode;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.CommentTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentList;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.FilterCriteria;
+import org.wordpress.android.ui.EmptyViewMessageType;
+import org.wordpress.android.ui.FilteredRecyclerView;
+import org.wordpress.android.ui.prefs.AppPrefs;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.xmlrpc.android.ApiHelper;
+import org.xmlrpc.android.ApiHelper.ErrorType;
+import org.xmlrpc.android.XMLRPCFault;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import de.greenrobot.event.EventBus;
+
+public class CommentsListFragment extends Fragment implements CommentAdapter.OnDataLoadedListener,
+ CommentAdapter.OnLoadMoreListener, CommentAdapter.OnSelectedItemsChangeListener, CommentAdapter.OnCommentPressedListener {
+
+ interface OnCommentSelectedListener {
+ void onCommentSelected(long commentId);
+ }
+
+ private boolean mIsUpdatingComments = false;
+ private boolean mCanLoadMoreComments = true;
+ boolean mHasAutoRefreshedComments = false;
+
+ private final CommentStatus[] commentStatuses = {CommentStatus.UNKNOWN, CommentStatus.UNAPPROVED,
+ CommentStatus.APPROVED, CommentStatus.TRASH, CommentStatus.SPAM};
+
+ private EmptyViewMessageType mEmptyViewMessageType = EmptyViewMessageType.NO_CONTENT;
+ private FilteredRecyclerView mFilteredCommentsView;
+ private CommentAdapter mAdapter;
+ private ActionMode mActionMode;
+ private CommentStatus mCommentStatusFilter;
+
+ private UpdateCommentsTask mUpdateCommentsTask;
+
+ public static final int COMMENTS_PER_PAGE = 30;
+
+ private CommentAdapterState mCommentAdapterState;
+
+
+ private boolean hasAdapter() {
+ return (mAdapter != null);
+ }
+
+ private int getSelectedCommentCount() {
+ return getAdapter().getSelectedCommentCount();
+ }
+
+ public void removeComment(Comment comment) {
+ if (hasAdapter() && comment != null) {
+ getAdapter().removeComment(comment);
+ }
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ Bundle extras = getActivity().getIntent().getExtras();
+ if (extras != null) {
+ mHasAutoRefreshedComments = extras.getBoolean(CommentsActivity.KEY_AUTO_REFRESHED);
+ mEmptyViewMessageType = EmptyViewMessageType.getEnumFromString(extras.getString(
+ CommentsActivity.KEY_EMPTY_VIEW_MESSAGE));
+ } else {
+ mHasAutoRefreshedComments = false;
+ mEmptyViewMessageType = EmptyViewMessageType.NO_CONTENT;
+ }
+
+ if (savedInstanceState != null) {
+ mCommentAdapterState = savedInstanceState.getParcelable(CommentAdapterState.KEY);
+ }
+
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
+ return;
+ }
+
+ // Restore the empty view's message
+ mFilteredCommentsView.updateEmptyView(mEmptyViewMessageType);
+
+ if (!mHasAutoRefreshedComments) {
+ updateComments(false);
+ mFilteredCommentsView.setRefreshing(true);
+ mHasAutoRefreshedComments = true;
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.comment_list_fragment, container, false);
+
+ mFilteredCommentsView = (FilteredRecyclerView) view.findViewById(R.id.filtered_recycler_view);
+ mFilteredCommentsView.setLogT(AppLog.T.COMMENTS);
+ mFilteredCommentsView.setFilterListener(new FilteredRecyclerView.FilterListener() {
+ @Override
+ public List<FilterCriteria> onLoadFilterCriteriaOptions(boolean refresh) {
+ @SuppressWarnings("unchecked")
+ ArrayList<FilterCriteria> criteria = new ArrayList();
+ Collections.addAll(criteria, commentStatuses);
+ return criteria;
+ }
+
+ @Override
+ public void onLoadFilterCriteriaOptionsAsync(FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener, boolean refresh) {
+ }
+
+ @Override
+ public void onLoadData() {
+ updateComments(false);
+ }
+
+ @Override
+ public void onFilterSelected(int position, FilterCriteria criteria) {
+ //trackCommentsAnalytics();
+ AppPrefs.setCommentsStatusFilter((CommentStatus) criteria);
+ mCommentStatusFilter = (CommentStatus) criteria;
+ finishActionMode();
+ }
+
+ @Override
+ public FilterCriteria onRecallSelection() {
+ mCommentStatusFilter = AppPrefs.getCommentsStatusFilter();
+ return mCommentStatusFilter;
+ }
+
+ @Override
+ public String onShowEmptyViewMessage(EmptyViewMessageType emptyViewMsgType) {
+
+ if (emptyViewMsgType == EmptyViewMessageType.NO_CONTENT) {
+ FilterCriteria filter = mFilteredCommentsView.getCurrentFilter();
+ if (filter == null || CommentStatus.UNKNOWN.equals(filter)) {
+ return getString(R.string.comments_empty_list);
+ } else {
+ switch (mCommentStatusFilter) {
+ case APPROVED:
+ return getString(R.string.comments_empty_list_filtered_approved);
+ case UNAPPROVED:
+ return getString(R.string.comments_empty_list_filtered_pending);
+ case SPAM:
+ return getString(R.string.comments_empty_list_filtered_spam);
+ case TRASH:
+ return getString(R.string.comments_empty_list_filtered_trashed);
+ default:
+ return getString(R.string.comments_empty_list);
+ }
+ }
+
+ } else {
+ int stringId = 0;
+ switch (emptyViewMsgType) {
+ case LOADING:
+ stringId = R.string.comments_fetching;
+ break;
+ case NETWORK_ERROR:
+ stringId = R.string.no_network_message;
+ break;
+ case PERMISSION_ERROR:
+ stringId = R.string.error_refresh_unauthorized_comments;
+ break;
+ case GENERIC_ERROR:
+ stringId = R.string.error_refresh_comments;
+ break;
+ }
+ return getString(stringId);
+ }
+
+ }
+
+ @Override
+ public void onShowCustomEmptyView(EmptyViewMessageType emptyViewMsgType) {
+ }
+ });
+
+ // the following will change the look and feel of the toolbar to match the current design
+ mFilteredCommentsView.setToolbarBackgroundColor(ContextCompat.getColor(getActivity(), R.color.blue_medium));
+ mFilteredCommentsView.setToolbarSpinnerTextColor(ContextCompat.getColor(getActivity(), R.color.white));
+ mFilteredCommentsView.setToolbarSpinnerDrawable(R.drawable.arrow);
+ mFilteredCommentsView.setToolbarLeftAndRightPadding(
+ getResources().getDimensionPixelSize(R.dimen.margin_filter_spinner),
+ getResources().getDimensionPixelSize(R.dimen.margin_none));
+
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (mFilteredCommentsView.getAdapter() == null) {
+ mFilteredCommentsView.setAdapter(getAdapter());
+ if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ ToastUtils.showToast(getActivity(), getString(R.string.error_refresh_comments_showing_older));
+ }
+ getAdapter().loadComments(mCommentStatusFilter);
+ }
+ }
+
+ public void setCommentStatusFilter(CommentStatus statusFilter) {
+ mCommentStatusFilter = statusFilter;
+ }
+
+ private void moderateSelectedComments(final CommentStatus newStatus) {
+ if (!NetworkUtils.checkConnection(getActivity())) return;
+
+ final CommentList selectedComments = getAdapter().getSelectedComments();
+ final CommentList updateComments = new CommentList();
+
+ // build list of comments whose status is different than passed
+ for (Comment comment : selectedComments) {
+ if (comment.getStatusEnum() != newStatus) {
+ setCommentIsModerating(comment.commentID, true);
+ updateComments.add(comment);
+ }
+
+ }
+ if (updateComments.size() == 0) return;
+
+ CommentActions.OnCommentsModeratedListener listener = new CommentActions.OnCommentsModeratedListener() {
+ @Override
+ public void onCommentsModerated(final CommentList moderatedComments) {
+ EventBus.getDefault().post(
+ new CommentEvents.CommentsBatchModerationFinishedEvent(moderatedComments, false));
+ }
+ };
+
+ getAdapter().clearSelectedComments();
+ finishActionMode();
+
+ CommentActions.moderateComments(
+ WordPress.getCurrentLocalTableBlogId(),
+ updateComments,
+ newStatus,
+ listener);
+ }
+
+
+ private void confirmDeleteComments() {
+ if (CommentStatus.TRASH.equals(mCommentStatusFilter)) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(
+ getActivity());
+ dialogBuilder.setTitle(getResources().getText(R.string.delete));
+ int resId = getAdapter().getSelectedCommentCount() > 1 ? R.string.dlg_sure_to_delete_comments : R.string.dlg_sure_to_delete_comment;
+ dialogBuilder.setMessage(getResources().getText(resId));
+ dialogBuilder.setPositiveButton(getResources().getText(R.string.yes),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ deleteSelectedComments(true);
+ }
+ });
+ dialogBuilder.setNegativeButton(
+ getResources().getText(R.string.no),
+ null);
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+
+ } else {
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(R.string.dlg_confirm_trash_comments);
+ builder.setTitle(R.string.trash);
+ builder.setCancelable(true);
+ builder.setPositiveButton(R.string.trash_yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ deleteSelectedComments(false);
+ }
+ });
+ builder.setNegativeButton(R.string.trash_no, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+ }
+
+ private void deleteSelectedComments(boolean deletePermanently) {
+ if (!NetworkUtils.checkConnection(getActivity())) return;
+
+
+ final CommentList selectedComments = getAdapter().getSelectedComments();
+
+ for (Comment comment : selectedComments) {
+ setCommentIsModerating(comment.commentID, true);
+ }
+
+ final CommentStatus newStatus = deletePermanently ? CommentStatus.DELETE : CommentStatus.TRASH;
+
+
+ CommentActions.OnCommentsModeratedListener listener = new CommentActions.OnCommentsModeratedListener() {
+ @Override
+ public void onCommentsModerated(final CommentList deletedComments) {
+ EventBus.getDefault().post(
+ new CommentEvents.CommentsBatchModerationFinishedEvent(deletedComments, true));
+ }
+ };
+
+ getAdapter().clearSelectedComments();
+ CommentActions.moderateComments(
+ WordPress.getCurrentLocalTableBlogId(), selectedComments, newStatus, listener);
+ }
+
+ void loadComments() {
+ // this is called from CommentsActivity when a comment was changed in the detail view,
+ // and the change will already be in SQLite so simply reload the comment adapter
+ // to show the change
+ getAdapter().loadComments(mCommentStatusFilter);
+ }
+
+ void updateEmptyView() {
+ //this is called from CommentsActivity in the case the last moment for a given type has been changed from that
+ //status, leaving the list empty, so we need to update the empty view. The method inside FilteredRecyclerView
+ //does the handling itself, so we only check for null here.
+ if (mFilteredCommentsView != null) {
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.NO_CONTENT);
+ }
+ }
+
+ /*
+ * get latest comments from server, or pass loadMore=true to get comments beyond the
+ * existing ones
+ */
+ void updateComments(boolean loadMore) {
+ if (mIsUpdatingComments) {
+ AppLog.w(AppLog.T.COMMENTS, "update comments task already running");
+ return;
+ } else if (!NetworkUtils.isNetworkAvailable(getActivity())) {
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
+ mFilteredCommentsView.setRefreshing(false);
+ ToastUtils.showToast(getActivity(), getString(R.string.error_refresh_comments_showing_older));
+ //we're offline, load/refresh whatever we have in our local db
+ getAdapter().loadComments(mCommentStatusFilter);
+ return;
+ }
+
+ //immediately load/refresh whatever we have in our local db as we wait for the API call to get latest results
+ if (!loadMore) {
+ getAdapter().loadComments(mCommentStatusFilter);
+ }
+
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.LOADING);
+
+ mUpdateCommentsTask = new UpdateCommentsTask(loadMore, mCommentStatusFilter);
+ mUpdateCommentsTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ public void setCommentIsModerating(long commentId, boolean isModerating) {
+ if (!hasAdapter()) return;
+
+ if (isModerating) {
+ getAdapter().addModeratingCommentId(commentId);
+ } else {
+ getAdapter().removeModeratingCommentId(commentId);
+ }
+ }
+
+ public String getEmptyViewMessage() {
+ return mEmptyViewMessageType.name();
+ }
+
+ /*
+ * task to retrieve latest comments from server
+ */
+ private class UpdateCommentsTask extends AsyncTask<Void, Void, CommentList> {
+ ErrorType mErrorType = ErrorType.NO_ERROR;
+ final boolean mIsLoadingMore;
+ final CommentStatus mStatusFilter;
+
+ private UpdateCommentsTask(boolean loadMore, CommentStatus statusFilter) {
+ mIsLoadingMore = loadMore;
+ mStatusFilter = statusFilter;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ mIsUpdatingComments = true;
+ if (mIsLoadingMore) {
+ mFilteredCommentsView.showLoadingProgress();
+ }
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ mIsUpdatingComments = false;
+ mUpdateCommentsTask = null;
+ mFilteredCommentsView.setRefreshing(false);
+ }
+
+ @Override
+ protected CommentList doInBackground(Void... args) {
+ if (!isAdded()) {
+ return null;
+ }
+
+ final Blog blog = WordPress.getCurrentBlog();
+ if (blog == null) {
+ mErrorType = ErrorType.INVALID_CURRENT_BLOG;
+ return null;
+ }
+
+ Map<String, Object> hPost = new HashMap<>();
+ if (mIsLoadingMore) {
+ int numExisting = getAdapter().getItemCount();
+ hPost.put("offset", numExisting);
+ hPost.put("number", COMMENTS_PER_PAGE);
+ } else {
+ hPost.put("number", COMMENTS_PER_PAGE);
+ }
+
+ if (mStatusFilter != null) {
+ //if this is UNKNOWN that means show ALL, i.e., do not apply filter
+ if (!mStatusFilter.equals(CommentStatus.UNKNOWN)) {
+ hPost.put("status", CommentStatus.toString(mStatusFilter));
+ }
+ }
+
+ Object[] params = {blog.getRemoteBlogId(),
+ blog.getUsername(),
+ blog.getPassword(),
+ hPost};
+ try {
+ return ApiHelper.refreshComments(blog, params, new ApiHelper.DatabasePersistCallback() {
+ @Override
+ public void onDataReadyToSave(List list) {
+ int localBlogId = blog.getLocalTableBlogId();
+
+ if (!mIsLoadingMore) { //existing comments should be deleted only if we are not "loading more"
+ CommentTable.deleteCommentsForBlogWithFilter(localBlogId, mStatusFilter);
+ }
+ CommentTable.saveComments(localBlogId, (CommentList) list);
+ }
+ });
+ } catch (XMLRPCFault xmlrpcFault) {
+ mErrorType = ErrorType.UNKNOWN_ERROR;
+ if (xmlrpcFault.getFaultCode() == 401) {
+ mErrorType = ErrorType.UNAUTHORIZED;
+ }
+ } catch (Exception e) {
+ mErrorType = ErrorType.UNKNOWN_ERROR;
+ }
+ return null;
+ }
+
+ protected void onPostExecute(CommentList comments) {
+
+ boolean isRefreshing = mFilteredCommentsView.isRefreshing();
+ mIsUpdatingComments = false;
+ mUpdateCommentsTask = null;
+
+ if (!isAdded()) return;
+
+ if (mIsLoadingMore) {
+ mFilteredCommentsView.hideLoadingProgress();
+ }
+ mFilteredCommentsView.setRefreshing(false);
+
+ if (isCancelled()) return;
+
+ mCanLoadMoreComments = (comments != null && comments.size() > 0);
+
+ // result will be null on error OR if no more comments exists
+ if (comments == null && !getActivity().isFinishing() && mErrorType != ErrorType.NO_ERROR) {
+ switch (mErrorType) {
+ case UNAUTHORIZED:
+ if (!mFilteredCommentsView.emptyViewIsVisible()) {
+ ToastUtils.showToast(getActivity(), getString(R.string.error_refresh_unauthorized_comments));
+ }
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.PERMISSION_ERROR);
+ return;
+ default:
+ ToastUtils.showToast(getActivity(), getString(R.string.error_refresh_comments));
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.GENERIC_ERROR);
+ return;
+ }
+ }
+
+ if (!getActivity().isFinishing()) {
+ if (comments != null && comments.size() > 0) {
+ getAdapter().loadComments(mStatusFilter);
+ } else {
+ if (isRefreshing) {
+ //if refreshing and no errors, we only want freshest stuff, so clear old data
+ getAdapter().clearComments();
+ }
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.NO_CONTENT);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ if (outState.isEmpty()) {
+ outState.putBoolean("bug_19917_fix", true);
+ }
+
+ if (hasAdapter()) {
+ outState.putParcelable(CommentAdapterState.KEY, getAdapter().getAdapterState());
+ }
+
+ super.onSaveInstanceState(outState);
+ }
+
+ /****
+ * Contextual ActionBar (CAB) routines
+ ***/
+ private void updateActionModeTitle() {
+ if (mActionMode == null)
+ return;
+ int numSelected = getSelectedCommentCount();
+ if (numSelected > 0) {
+ mActionMode.setTitle(Integer.toString(numSelected));
+ } else {
+ mActionMode.setTitle("");
+ }
+ }
+
+ private void finishActionMode() {
+ if (mActionMode != null) {
+ mActionMode.finish();
+ }
+ }
+
+ private final class ActionModeCallback implements ActionMode.Callback {
+ @Override
+ public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
+ mActionMode = actionMode;
+ MenuInflater inflater = actionMode.getMenuInflater();
+ inflater.inflate(R.menu.menu_comments_cab, menu);
+ mFilteredCommentsView.setSwipeToRefreshEnabled(false);
+ return true;
+ }
+
+ private void setItemEnabled(Menu menu, int menuId, boolean isEnabled) {
+ final MenuItem item = menu.findItem(menuId);
+ if (item == null || item.isEnabled() == isEnabled)
+ return;
+ item.setEnabled(isEnabled);
+ if (item.getIcon() != null) {
+ // must mutate the drawable to avoid affecting other instances of it
+ Drawable icon = item.getIcon().mutate();
+ icon.setAlpha(isEnabled ? 255 : 128);
+ item.setIcon(icon);
+ }
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
+ final CommentList selectedComments = getAdapter().getSelectedComments();
+
+ boolean hasSelection = (selectedComments.size() > 0);
+ boolean hasApproved = hasSelection && selectedComments.hasAnyWithStatus(CommentStatus.APPROVED);
+ boolean hasUnapproved = hasSelection && selectedComments.hasAnyWithStatus(CommentStatus.UNAPPROVED);
+ boolean hasSpam = hasSelection && selectedComments.hasAnyWithStatus(CommentStatus.SPAM);
+ boolean hasAnyNonSpam = hasSelection && selectedComments.hasAnyWithoutStatus(CommentStatus.SPAM);
+ boolean hasTrash = hasSelection && selectedComments.hasAnyWithStatus(CommentStatus.TRASH);
+
+ setItemEnabled(menu, R.id.menu_approve, hasUnapproved || hasSpam || hasTrash);
+ setItemEnabled(menu, R.id.menu_unapprove, hasApproved);
+ setItemEnabled(menu, R.id.menu_spam, hasAnyNonSpam);
+ setItemEnabled(menu, R.id.menu_trash, hasSelection);
+
+ final MenuItem trashItem = menu.findItem(R.id.menu_trash);
+ if (trashItem != null) {
+ if (CommentStatus.TRASH.equals(mCommentStatusFilter)) {
+ trashItem.setTitle(R.string.mnu_comment_delete_permanently);
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
+ int numSelected = getSelectedCommentCount();
+ if (numSelected == 0)
+ return false;
+
+ int i = menuItem.getItemId();
+ if (i == R.id.menu_approve) {
+ moderateSelectedComments(CommentStatus.APPROVED);
+ return true;
+ } else if (i == R.id.menu_unapprove) {
+ moderateSelectedComments(CommentStatus.UNAPPROVED);
+ return true;
+ } else if (i == R.id.menu_spam) {
+ moderateSelectedComments(CommentStatus.SPAM);
+ return true;
+ } else if (i == R.id.menu_trash) {// unlike the other status changes, we ask the user to confirm trashing
+ confirmDeleteComments();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ getAdapter().setEnableSelection(false);
+ mFilteredCommentsView.setSwipeToRefreshEnabled(true);
+ mActionMode = null;
+ }
+ }
+
+ private CommentAdapter getAdapter() {
+ if (mAdapter == null) {
+ mAdapter = new CommentAdapter(getActivity(), WordPress.getCurrentLocalTableBlogId());
+ mAdapter.setInitialState(mCommentAdapterState);
+ mAdapter.setOnCommentPressedListener(this);
+ mAdapter.setOnDataLoadedListener(this);
+ mAdapter.setOnLoadMoreListener(this);
+ mAdapter.setOnSelectedItemsChangeListener(this);
+ }
+
+ return mAdapter;
+ }
+
+
+ // adapter calls this when selected comments have changed (CAB)
+ @Override
+ public void onSelectedItemsChanged() {
+ if (mActionMode != null) {
+ if (getSelectedCommentCount() == 0) {
+ mActionMode.finish();
+ } else {
+ updateActionModeTitle();
+ // must invalidate to ensure onPrepareActionMode is called
+ mActionMode.invalidate();
+ }
+ }
+ }
+
+ @Override
+ public void onCommentPressed(int position, View view) {
+ // if the comment is being moderated ignore the press
+ Comment comment = getAdapter().getItem(position);
+ if (!isCommentSelectable(comment)) {
+ return;
+ }
+
+ if (mActionMode == null) {
+ mFilteredCommentsView.invalidate();
+ if (getActivity() instanceof OnCommentSelectedListener) {
+ ((OnCommentSelectedListener) getActivity()).onCommentSelected(comment.commentID);
+ }
+ } else {
+ getAdapter().toggleItemSelected(position, view);
+ }
+ }
+
+ @Override
+ public void onCommentLongPressed(int position, View view) {
+ // if the comment is being moderated ignore the press
+ Comment comment = getAdapter().getItem(position);
+ if (!isCommentSelectable(comment)) {
+ return;
+ }
+
+ // enable CAB if it's not already enabled
+ if (mActionMode == null) {
+ if (getActivity() instanceof AppCompatActivity) {
+ ((AppCompatActivity) getActivity()).startSupportActionMode(new ActionModeCallback());
+ getAdapter().setEnableSelection(true);
+ getAdapter().setItemSelected(position, true, view);
+ }
+ } else {
+ getAdapter().toggleItemSelected(position, view);
+ }
+ }
+
+ private boolean isCommentSelectable(Comment comment){
+ return comment != null && !getAdapter().isModeratingCommentId(comment.commentID);
+ }
+
+ private boolean shouldRestoreCab() {
+ return hasAdapter() && !getAdapter().getSelectedCommentsId().isEmpty() && mActionMode == null;
+ }
+
+ private void restoreCab() {
+ if (getActivity() instanceof AppCompatActivity) {
+ ((AppCompatActivity) getActivity()).startSupportActionMode(new ActionModeCallback());
+ updateActionModeTitle();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ EventBus.getDefault().register(this);
+ }
+
+ @Override
+ public void onStop() {
+ EventBus.getDefault().unregister(this);
+ super.onStop();
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(CommentEvents.CommentModerationFinishedEvent event) {
+ if (!isAdded()) return;
+
+ setCommentIsModerating(event.getCommentId(), false);
+
+ if (!event.isSuccess()) {
+ ToastUtils.showToast(getActivity(), R.string.error_moderate_comment, ToastUtils.Duration.LONG);
+ }
+
+ if (event.isCommentsRefreshRequired() || event.getNewStatus() != mCommentStatusFilter) {
+ loadComments();
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public void onEventMainThread(CommentEvents.CommentsBatchModerationFinishedEvent moderatedComments) {
+ if (!isAdded()) return;
+
+ if (moderatedComments.getComments().size() > 0) {
+ for (Comment comment : moderatedComments.getComments()) {
+ setCommentIsModerating(comment.commentID, false);
+ }
+
+ if (moderatedComments.isDeleted()) {
+ getAdapter().deleteComments(moderatedComments.getComments());
+ } else {
+ getAdapter().replaceComments(moderatedComments.getComments());
+ }
+
+ loadComments();
+ } else {
+ ToastUtils.showToast(getActivity(), R.string.error_moderate_comment);
+ }
+ }
+
+
+ // called after comments have been loaded
+ @Override
+ public void onDataLoaded(boolean isEmpty) {
+ if (!isAdded()) return;
+
+ if (!isEmpty) {
+ // After comments are loaded, we should check if some of them are selected and show CAB if necessary
+ if (shouldRestoreCab()) {
+ restoreCab();
+ }
+
+ // Hide the empty view if there are already some displayed comments
+ mFilteredCommentsView.hideEmptyView();
+ } else if (!mIsUpdatingComments) {
+ // Change LOADING to NO_CONTENT message
+ mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.NO_CONTENT);
+ }
+ }
+
+ @Override
+ public void onLoadMore() {
+ if (mCanLoadMoreComments && !mIsUpdatingComments) {
+ updateComments(true);
+ }
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/EditCommentActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/EditCommentActivity.java
new file mode 100644
index 000000000..fc784483c
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/EditCommentActivity.java
@@ -0,0 +1,440 @@
+package org.wordpress.android.ui.comments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+
+import com.android.volley.VolleyError;
+import com.simperium.client.BucketObjectMissingException;
+import com.wordpress.rest.RestRequest;
+
+import org.json.JSONObject;
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.CommentTable;
+import org.wordpress.android.models.Blog;
+import org.wordpress.android.models.Comment;
+import org.wordpress.android.models.CommentStatus;
+import org.wordpress.android.models.Note;
+import org.wordpress.android.ui.ActivityId;
+import org.wordpress.android.ui.notifications.utils.SimperiumUtils;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.EditTextUtils;
+import org.wordpress.android.util.ToastUtils;
+import org.wordpress.android.util.VolleyUtils;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlrpc.android.ApiHelper.Method;
+import org.xmlrpc.android.XMLRPCClientInterface;
+import org.xmlrpc.android.XMLRPCException;
+import org.xmlrpc.android.XMLRPCFactory;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class EditCommentActivity extends AppCompatActivity {
+ static final String ARG_LOCAL_BLOG_ID = "blog_id";
+ static final String ARG_COMMENT_ID = "comment_id";
+ static final String ARG_NOTE_ID = "note_id";
+
+ private static final int ID_DIALOG_SAVING = 0;
+
+ private int mLocalBlogId;
+ private long mCommentId;
+ private Comment mComment;
+ private Note mNote;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.comment_edit_activity);
+ setTitle(getString(R.string.edit_comment));
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ loadComment(getIntent());
+
+ ActivityId.trackLastActivity(ActivityId.COMMENT_EDITOR);
+ }
+
+ private void loadComment(Intent intent) {
+ if (intent == null) {
+ showErrorAndFinish();
+ return;
+ }
+
+ mLocalBlogId = intent.getIntExtra(ARG_LOCAL_BLOG_ID, 0);
+ mCommentId = intent.getLongExtra(ARG_COMMENT_ID, 0);
+ final String noteId = intent.getStringExtra(ARG_NOTE_ID);
+ if (noteId == null) {
+ mComment = CommentTable.getComment(mLocalBlogId, mCommentId);
+ if (mComment == null) {
+ showErrorAndFinish();
+ return;
+ }
+
+ configureViews();
+ } else {
+ if (SimperiumUtils.getNotesBucket() != null) {
+ try {
+ mNote = SimperiumUtils.getNotesBucket().get(noteId);
+ requestFullCommentForNote(mNote);
+ } catch (BucketObjectMissingException e) {
+ showErrorAndFinish();
+ }
+ }
+ }
+ }
+
+ private void showErrorAndFinish() {
+ ToastUtils.showToast(this, R.string.error_load_comment);
+ finish();
+ }
+
+ private void configureViews() {
+ final EditText editAuthorName = (EditText) this.findViewById(R.id.author_name);
+ editAuthorName.setText(mComment.getAuthorName());
+
+ final EditText editAuthorEmail = (EditText) this.findViewById(R.id.author_email);
+ editAuthorEmail.setText(mComment.getAuthorEmail());
+
+ final EditText editAuthorUrl = (EditText) this.findViewById(R.id.author_url);
+ editAuthorUrl.setText(mComment.getAuthorUrl());
+
+ // REST API can currently only edit comment content
+ if (mNote != null) {
+ editAuthorName.setVisibility(View.GONE);
+ editAuthorEmail.setVisibility(View.GONE);
+ editAuthorUrl.setVisibility(View.GONE);
+ }
+
+ final EditText editContent = (EditText) this.findViewById(R.id.edit_comment_content);
+ editContent.setText(mComment.getCommentText());
+
+ // show error when comment content is empty
+ editContent.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ @Override
+ public void afterTextChanged(Editable s) {
+ boolean hasError = (editContent.getError() != null);
+ boolean hasText = (s != null && s.length() > 0);
+ if (!hasText && !hasError) {
+ editContent.setError(getString(R.string.content_required));
+ } else if (hasText && hasError) {
+ editContent.setError(null);
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.edit_comment, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem item) {
+ int i = item.getItemId();
+ if (i == android.R.id.home) {
+ onBackPressed();
+ return true;
+ } else if (i == R.id.menu_save_comment) {
+ saveComment();
+ return true;
+ } else {
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private String getEditTextStr(int resId) {
+ final EditText edit = (EditText) findViewById(resId);
+ return EditTextUtils.getText(edit);
+ }
+
+ private void saveComment() {
+ // make sure comment content was entered
+ final EditText editContent = (EditText) findViewById(R.id.edit_comment_content);
+ if (EditTextUtils.isEmpty(editContent)) {
+ editContent.setError(getString(R.string.content_required));
+ return;
+ }
+
+ // return immediately if comment hasn't changed
+ if (!isCommentEdited()) {
+ ToastUtils.showToast(this, R.string.toast_comment_unedited);
+ return;
+ }
+
+ // make sure we have an active connection
+ if (!NetworkUtils.checkConnection(this))
+ return;
+
+ if (mNote != null) {
+ // Edit comment via REST API :)
+ showSaveDialog();
+ WordPress.getRestClientUtils().editCommentContent(mNote.getSiteId(),
+ mNote.getCommentId(),
+ EditTextUtils.getText(editContent),
+ new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject response) {
+ if (isFinishing()) return;
+
+ dismissSaveDialog();
+ setResult(RESULT_OK);
+ finish();
+ }
+ }, new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ if (isFinishing()) return;
+
+ dismissSaveDialog();
+ showEditErrorAlert();
+ }
+ });
+ } else {
+ // Edit comment via XML-RPC :(
+ if (mIsUpdateTaskRunning)
+ AppLog.w(AppLog.T.COMMENTS, "update task already running");
+ new UpdateCommentTask().execute();
+ }
+ }
+
+ /*
+ * returns true if user made any changes to the comment
+ */
+ private boolean isCommentEdited() {
+ if (mComment == null)
+ return false;
+
+ final String authorName = getEditTextStr(R.id.author_name);
+ final String authorEmail = getEditTextStr(R.id.author_email);
+ final String authorUrl = getEditTextStr(R.id.author_url);
+ final String content = getEditTextStr(R.id.edit_comment_content);
+
+ return !(authorName.equals(mComment.getAuthorName())
+ && authorEmail.equals(mComment.getAuthorEmail())
+ && authorUrl.equals(mComment.getAuthorUrl())
+ && content.equals(mComment.getCommentText()));
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ if (id == ID_DIALOG_SAVING) {
+ ProgressDialog savingDialog = new ProgressDialog(this);
+ savingDialog.setMessage(getResources().getText(R.string.saving_changes));
+ savingDialog.setIndeterminate(true);
+ savingDialog.setCancelable(true);
+ return savingDialog;
+ }
+ return super.onCreateDialog(id);
+ }
+
+ private void showSaveDialog() {
+ showDialog(ID_DIALOG_SAVING);
+ }
+
+ private void dismissSaveDialog() {
+ try {
+ dismissDialog(ID_DIALOG_SAVING);
+ } catch (IllegalArgumentException e) {
+ // dialog doesn't exist
+ }
+ }
+
+ /*
+ * AsyncTask to save comment to server
+ */
+ private boolean mIsUpdateTaskRunning = false;
+ private class UpdateCommentTask extends AsyncTask<Void, Void, Boolean> {
+ @Override
+ protected void onPreExecute() {
+ mIsUpdateTaskRunning = true;
+ showSaveDialog();
+ }
+ @Override
+ protected void onCancelled() {
+ mIsUpdateTaskRunning = false;
+ dismissSaveDialog();
+ }
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ final Blog blog;
+ blog = WordPress.wpDB.instantiateBlogByLocalId(mLocalBlogId);
+ if (blog == null) {
+ AppLog.e(AppLog.T.COMMENTS, "Invalid local blog id:" + mLocalBlogId);
+ return false;
+ }
+ final String authorName = getEditTextStr(R.id.author_name);
+ final String authorEmail = getEditTextStr(R.id.author_email);
+ final String authorUrl = getEditTextStr(R.id.author_url);
+ final String content = getEditTextStr(R.id.edit_comment_content);
+
+ final Map<String, String> postHash = new HashMap<>();
+
+ // using CommentStatus.toString() rather than getStatus() ensures that the XML-RPC
+ // status value is used - important since comment may have been loaded via the
+ // REST API, which uses different status values
+ postHash.put("status", CommentStatus.toString(mComment.getStatusEnum()));
+ postHash.put("content", content);
+ postHash.put("author", authorName);
+ postHash.put("author_url", authorUrl);
+ postHash.put("author_email", authorEmail);
+
+ XMLRPCClientInterface client = XMLRPCFactory.instantiate(blog.getUri(), blog.getHttpuser(),
+ blog.getHttppassword());
+ Object[] xmlParams = {blog.getRemoteBlogId(), blog.getUsername(), blog.getPassword(), Long.toString(
+ mCommentId), postHash};
+
+ try {
+ Object result = client.call(Method.EDIT_COMMENT, xmlParams);
+ boolean isSaved = (result != null && Boolean.parseBoolean(result.toString()));
+ if (isSaved) {
+ mComment.setAuthorEmail(authorEmail);
+ mComment.setAuthorUrl(authorUrl);
+ mComment.setAuthorName(authorName);
+ mComment.setCommentText(content);
+ CommentTable.updateComment(mLocalBlogId, mComment);
+ }
+ return isSaved;
+ } catch (XMLRPCException e) {
+ AppLog.e(AppLog.T.COMMENTS, e);
+ return false;
+ } catch (IOException e) {
+ AppLog.e(AppLog.T.COMMENTS, e);
+ return false;
+ } catch (XmlPullParserException e) {
+ AppLog.e(AppLog.T.COMMENTS, e);
+ return false;
+ }
+ }
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (isFinishing()) return;
+
+ mIsUpdateTaskRunning = false;
+ dismissSaveDialog();
+
+ if (result) {
+ setResult(RESULT_OK);
+ finish();
+ } else {
+ // alert user to error
+ showEditErrorAlert();
+ }
+ }
+ }
+
+ private void showEditErrorAlert() {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(EditCommentActivity.this);
+ dialogBuilder.setTitle(getResources().getText(R.string.error));
+ dialogBuilder.setMessage(R.string.error_edit_comment);
+ dialogBuilder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // just close the dialog
+ }
+ });
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+ }
+
+ // Request a comment via the REST API for a note
+ private void requestFullCommentForNote(Note note) {
+ if (isFinishing()) return;
+ final ProgressBar progress = (ProgressBar)findViewById(R.id.edit_comment_progress);
+ final View editContainer = findViewById(R.id.edit_comment_container);
+
+ if (progress == null || editContainer == null) {
+ return;
+ }
+
+ editContainer.setVisibility(View.GONE);
+ progress.setVisibility(View.VISIBLE);
+
+ RestRequest.Listener restListener = new RestRequest.Listener() {
+ @Override
+ public void onResponse(JSONObject jsonObject) {
+ if (!isFinishing()) {
+ progress.setVisibility(View.GONE);
+ editContainer.setVisibility(View.VISIBLE);
+ Comment comment = Comment.fromJSON(jsonObject);
+ if (comment != null) {
+ mComment = comment;
+ configureViews();
+ } else {
+ showErrorAndFinish();
+ }
+ }
+ }
+ };
+ RestRequest.ErrorListener restErrListener = new RestRequest.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError volleyError) {
+ AppLog.e(AppLog.T.COMMENTS, VolleyUtils.errStringFromVolleyError(volleyError), volleyError);
+ if (!isFinishing()) {
+ progress.setVisibility(View.GONE);
+ showErrorAndFinish();
+ }
+ }
+ };
+
+ final String path = String.format("/sites/%s/comments/%s", note.getSiteId(), note.getCommentId());
+ WordPress.getRestClientUtils().get(path, restListener, restErrListener);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (isCommentEdited()) {
+ AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(
+ EditCommentActivity.this);
+ dialogBuilder.setTitle(getResources().getText(R.string.cancel_edit));
+ dialogBuilder.setMessage(getResources().getText(R.string.sure_to_cancel_edit_comment));
+ dialogBuilder.setPositiveButton(getResources().getText(R.string.yes),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ finish();
+ }
+ });
+ dialogBuilder.setNegativeButton(
+ getResources().getText(R.string.no),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // just close the dialog
+ }
+ });
+ dialogBuilder.setCancelable(true);
+ dialogBuilder.create().show();
+ } else {
+ super.onBackPressed();
+ }
+ }
+}