blob: eafe02723f6525c44bf4305b7edd36621c916725 [file] [log] [blame]
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);
}
}