package org.wordpress.android.ui.reader;
import android.app.Activity;
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.LinearLayoutManager;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.wordpress.android.R;
import org.wordpress.android.WordPress;
import org.wordpress.android.analytics.AnalyticsTracker;
import org.wordpress.android.datasets.ReaderCommentTable;
import org.wordpress.android.datasets.ReaderPostTable;
import org.wordpress.android.datasets.SuggestionTable;
import org.wordpress.android.fluxc.store.AccountStore;
import org.wordpress.android.models.ReaderComment;
import org.wordpress.android.models.ReaderPost;
import org.wordpress.android.models.Suggestion;
import org.wordpress.android.ui.ActivityLauncher;
import org.wordpress.android.ui.RequestCodes;
import org.wordpress.android.ui.reader.ReaderPostPagerActivity.DirectOperation;
import org.wordpress.android.ui.reader.actions.ReaderActions;
import org.wordpress.android.ui.reader.actions.ReaderCommentActions;
import org.wordpress.android.ui.reader.actions.ReaderPostActions;
import org.wordpress.android.ui.reader.adapters.ReaderCommentAdapter;
import org.wordpress.android.ui.reader.services.ReaderCommentService;
import org.wordpress.android.ui.reader.views.ReaderRecyclerView;
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.AnalyticsUtils;
import org.wordpress.android.util.AppLog;
import org.wordpress.android.util.AppLog.T;
import org.wordpress.android.util.DisplayUtils;
import org.wordpress.android.util.EditTextUtils;
import org.wordpress.android.util.NetworkUtils;
import org.wordpress.android.util.ToastUtils;
import org.wordpress.android.util.WPActivityUtils;
import org.wordpress.android.util.helpers.SwipeToRefreshHelper;
import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout;
import org.wordpress.android.widgets.RecyclerItemDecoration;
import org.wordpress.android.widgets.SuggestionAutoCompleteText;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import de.greenrobot.event.EventBus;
public class ReaderCommentListActivity extends AppCompatActivity {
private static final String KEY_REPLY_TO_COMMENT_ID = "reply_to_comment_id";
private static final String KEY_HAS_UPDATED_COMMENTS = "has_updated_comments";
private long mPostId;
private long mBlogId;
private ReaderPost mPost;
private ReaderCommentAdapter mCommentAdapter;
private SuggestionAdapter mSuggestionAdapter;
private SuggestionServiceConnectionManager mSuggestionServiceConnectionManager;
private SwipeToRefreshHelper mSwipeToRefreshHelper;
private ReaderRecyclerView mRecyclerView;
private SuggestionAutoCompleteText mEditComment;
private View mSubmitReplyBtn;
private ViewGroup mCommentBox;
private boolean mIsUpdatingComments;
private boolean mHasUpdatedComments;
private boolean mIsSubmittingComment;
private DirectOperation mDirectOperation;
private long mReplyToCommentId;
private long mCommentId;
private int mRestorePosition;
private String mInterceptedUri;
private boolean mBackFromLogin;
@Inject AccountStore mAccountStore;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((WordPress) getApplication()).component().inject(this);
setContentView(R.layout.reader_activity_comment_list);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
if (toolbar != null) {
setSupportActionBar(toolbar);
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
}
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
}
if (savedInstanceState != null) {
mBlogId = savedInstanceState.getLong(ReaderConstants.ARG_BLOG_ID);
mPostId = savedInstanceState.getLong(ReaderConstants.ARG_POST_ID);
mRestorePosition = savedInstanceState.getInt(ReaderConstants.KEY_RESTORE_POSITION);
mHasUpdatedComments = savedInstanceState.getBoolean(KEY_HAS_UPDATED_COMMENTS);
mInterceptedUri = savedInstanceState.getString(ReaderConstants.ARG_INTERCEPTED_URI);
} else {
mBlogId = getIntent().getLongExtra(ReaderConstants.ARG_BLOG_ID, 0);
mPostId = getIntent().getLongExtra(ReaderConstants.ARG_POST_ID, 0);
mDirectOperation = (DirectOperation) getIntent()
.getSerializableExtra(ReaderConstants.ARG_DIRECT_OPERATION);
mCommentId = getIntent().getLongExtra(ReaderConstants.ARG_COMMENT_ID, 0);
mInterceptedUri = getIntent().getStringExtra(ReaderConstants.ARG_INTERCEPTED_URI);
// we need to re-request comments every time this activity is shown in order to
// correctly reflect deletions and nesting changes - skipped when there's no
// connection so we can show existing comments while offline
if (NetworkUtils.isNetworkAvailable(this)) {
ReaderCommentTable.purgeCommentsForPost(mBlogId, mPostId);
}
}
mSwipeToRefreshHelper = new SwipeToRefreshHelper(this,
(CustomSwipeRefreshLayout) findViewById(R.id.swipe_to_refresh),
new SwipeToRefreshHelper.RefreshListener() {
@Override
public void onRefreshStarted() {
updatePostAndComments();
}
});
mRecyclerView = (ReaderRecyclerView) findViewById(R.id.recycler_view);
int spacingHorizontal = 0;
int spacingVertical = DisplayUtils.dpToPx(this, 1);
mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical));
mCommentBox = (ViewGroup) findViewById(R.id.layout_comment_box);
mEditComment = (SuggestionAutoCompleteText) mCommentBox.findViewById(R.id.edit_comment);
mEditComment.getAutoSaveTextHelper().setUniqueId(String.format(Locale.US, "%d%d", mPostId, mBlogId));
mSubmitReplyBtn = mCommentBox.findViewById(R.id.btn_submit_reply);
if (!loadPost()) {
ToastUtils.showToast(this, R.string.reader_toast_err_get_post);
finish();
return;
}
mRecyclerView.setAdapter(getCommentAdapter());
if (savedInstanceState != null) {
setReplyToCommentId(savedInstanceState.getLong(KEY_REPLY_TO_COMMENT_ID), false);
}
refreshComments();
mSuggestionServiceConnectionManager = new SuggestionServiceConnectionManager(this, mBlogId);
mSuggestionAdapter = SuggestionUtils.setupSuggestions(mBlogId, this, mSuggestionServiceConnectionManager,
mPost.isWP());
if (mSuggestionAdapter != null) {
mEditComment.setAdapter(mSuggestionAdapter);
}
AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_COMMENTS_OPENED, mPost);
}
private final View.OnClickListener mSignInClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isFinishing()) return;
AnalyticsUtils.trackWithInterceptedUri(AnalyticsTracker.Stat.READER_SIGN_IN_INITIATED, mInterceptedUri);
ActivityLauncher.loginWithoutMagicLink(ReaderCommentListActivity.this);
}
};
private void updatePostAndComments() {
//to do a complete refresh we need to get updated post and new comments
ReaderPostActions.updatePost(mPost, new ReaderActions.UpdateResultListener() {
@Override
public void onUpdateResult(ReaderActions.UpdateResult result) {
if (isFinishing()) {
return;
}
if (result.isNewOrChanged()) {
getCommentAdapter().setPost(mPost); //pass updated post to the adapter
ReaderCommentTable.purgeCommentsForPost(mBlogId, mPostId); //clear all the previous comments
updateComments(false, false); //load first page of comments
} else {
setRefreshing(false);
}
}
});
}
@Override
public void onResume() {
super.onResume();
EventBus.getDefault().register(this);
if (mBackFromLogin) {
if (NetworkUtils.isNetworkAvailable(this)) {
// purge and reload the comments since logged in changes some info (example: isLikedByCurrentUser)
ReaderCommentTable.purgeCommentsForPost(mBlogId, mPostId);
updatePostAndComments();
}
// clear up the back-from-login flag anyway
mBackFromLogin = false;
}
refreshComments();
}
@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 == mBlogId && mSuggestionAdapter != null) {
List<Suggestion> suggestions = SuggestionTable.getSuggestionsForSite(event.mRemoteBlogId);
mSuggestionAdapter.setSuggestionList(suggestions);
}
}
@Override
public void onPause() {
super.onPause();
EventBus.getDefault().unregister(this);
}
private void setReplyToCommentId(long commentId, boolean doFocus) {
mReplyToCommentId = commentId;
mEditComment.setHint(mReplyToCommentId == 0 ?
R.string.reader_hint_comment_on_post : R.string.reader_hint_comment_on_comment);
if (doFocus) {
mEditComment.postDelayed(new Runnable() {
@Override
public void run() {
final boolean isFocusableInTouchMode = mEditComment.isFocusableInTouchMode();
mEditComment.setFocusableInTouchMode(true);
EditTextUtils.showSoftInput(mEditComment);
mEditComment.setFocusableInTouchMode(isFocusableInTouchMode);
setupReplyToComment();
}
}, 200);
} else {
setupReplyToComment();
}
}
private void setupReplyToComment() {
// if a comment is being replied to, highlight it and scroll it to the top so the user can
// see which comment they're replying to - note that scrolling is delayed to give time for
// listView to reposition due to soft keyboard appearing
if (mReplyToCommentId != 0) {
getCommentAdapter().setHighlightCommentId(mReplyToCommentId, false);
getCommentAdapter().notifyDataSetChanged();
mRecyclerView.postDelayed(new Runnable() {
@Override
public void run() {
scrollToCommentId(mReplyToCommentId);
}
}, 300);
// reset to replying to the post when user hasn't entered any text and hits
// the back button in the editText to hide the soft keyboard
mEditComment.setOnBackListener(new SuggestionAutoCompleteText.OnEditTextBackListener() {
@Override
public void onEditTextBack() {
if (EditTextUtils.isEmpty(mEditComment)) {
setReplyToCommentId(0, false);
}
}
});
} else {
mEditComment.setOnBackListener(null);
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putLong(ReaderConstants.ARG_BLOG_ID, mBlogId);
outState.putLong(ReaderConstants.ARG_POST_ID, mPostId);
outState.putInt(ReaderConstants.KEY_RESTORE_POSITION, getCurrentPosition());
outState.putLong(KEY_REPLY_TO_COMMENT_ID, mReplyToCommentId);
outState.putBoolean(KEY_HAS_UPDATED_COMMENTS, mHasUpdatedComments);
outState.putString(ReaderConstants.ARG_INTERCEPTED_URI, mInterceptedUri);
super.onSaveInstanceState(outState);
}
private void showCommentsClosedMessage(boolean show) {
TextView txtCommentsClosed = (TextView) findViewById(R.id.text_comments_closed);
if (txtCommentsClosed != null) {
txtCommentsClosed.setVisibility(show ? View.VISIBLE : View.GONE);
}
}
private boolean loadPost() {
mPost = ReaderPostTable.getBlogPost(mBlogId, mPostId, true);
if (mPost == null) {
return false;
}
TextView txtCommentsClosed = (TextView) findViewById(R.id.text_comments_closed);
if (!mAccountStore.hasAccessToken()) {
mCommentBox.setVisibility(View.GONE);
showCommentsClosedMessage(false);
} else if (mPost.isCommentsOpen) {
mCommentBox.setVisibility(View.VISIBLE);
showCommentsClosedMessage(false);
mEditComment.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) {
submitComment();
}
return false;
}
});
mSubmitReplyBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
submitComment();
}
});
} else {
mCommentBox.setVisibility(View.GONE);
mEditComment.setEnabled(false);
showCommentsClosedMessage(true);
}
return true;
}
@Override
public void onDestroy() {
if (mSuggestionServiceConnectionManager != null) {
mSuggestionServiceConnectionManager.unbindFromService();
}
super.onDestroy();
}
private boolean hasCommentAdapter() {
return (mCommentAdapter != null);
}
private ReaderCommentAdapter getCommentAdapter() {
if (mCommentAdapter == null) {
mCommentAdapter = new ReaderCommentAdapter(WPActivityUtils.getThemedContext(this), getPost());
// adapter calls this when user taps reply icon
mCommentAdapter.setReplyListener(new ReaderCommentAdapter.RequestReplyListener() {
@Override
public void onRequestReply(long commentId) {
setReplyToCommentId(commentId, true);
}
});
// Enable post title click if we came here directly from notifications or deep linking
if (mDirectOperation != null) {
mCommentAdapter.enableHeaderClicks();
}
// adapter calls this when data has been loaded & displayed
mCommentAdapter.setDataLoadedListener(new ReaderInterfaces.DataLoadedListener() {
@Override
public void onDataLoaded(boolean isEmpty) {
if (!isFinishing()) {
if (isEmpty || !mHasUpdatedComments) {
updateComments(isEmpty, false);
} else if (mCommentId > 0 || mDirectOperation != null) {
if (mCommentId > 0) {
// Scroll to the commentId once if it was passed to this activity
smoothScrollToCommentId(mCommentId);
}
doDirectOperation();
} else if (mRestorePosition > 0) {
mRecyclerView.scrollToPosition(mRestorePosition);
}
mRestorePosition = 0;
checkEmptyView();
}
}
});
// adapter uses this to request more comments from server when it reaches the end and
// detects that more comments exist on the server than are stored locally
mCommentAdapter.setDataRequestedListener(new ReaderActions.DataRequestedListener() {
@Override
public void onRequestData() {
if (!mIsUpdatingComments) {
AppLog.i(T.READER, "reader comments > requesting next page of comments");
updateComments(true, true);
}
}
});
}
return mCommentAdapter;
}
private void doDirectOperation() {
if (mDirectOperation != null) {
switch (mDirectOperation) {
case COMMENT_JUMP:
mCommentAdapter.setHighlightCommentId(mCommentId, false);
// clear up the direct operation vars. Only performing it once.
mDirectOperation = null;
mCommentId = 0;
break;
case COMMENT_REPLY:
setReplyToCommentId(mCommentId, mAccountStore.hasAccessToken());
// clear up the direct operation vars. Only performing it once.
mDirectOperation = null;
mCommentId = 0;
break;
case COMMENT_LIKE:
getCommentAdapter().setHighlightCommentId(mCommentId, false);
if (!mAccountStore.hasAccessToken()) {
Snackbar.make(mRecyclerView,
R.string.reader_snackbar_err_cannot_like_post_logged_out,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.sign_in, mSignInClickListener)
.show();
} else {
ReaderComment comment = ReaderCommentTable.getComment(mPost.blogId, mPost.postId, mCommentId);
if (comment == null) {
ToastUtils.showToast(ReaderCommentListActivity.this,
R.string.reader_toast_err_comment_not_found);
} else if (comment.isLikedByCurrentUser) {
ToastUtils.showToast(ReaderCommentListActivity.this,
R.string.reader_toast_err_already_liked);
} else {
long wpComUserId = mAccountStore.getAccount().getUserId();
if (ReaderCommentActions.performLikeAction(comment, true, wpComUserId) &&
getCommentAdapter().refreshComment(mCommentId)) {
getCommentAdapter().setAnimateLikeCommentId(mCommentId);
AnalyticsUtils.trackWithReaderPostDetails(
AnalyticsTracker.Stat.READER_ARTICLE_COMMENT_LIKED, mPost);
} else {
ToastUtils.showToast(ReaderCommentListActivity.this,
R.string.reader_toast_err_generic);
}
}
// clear up the direct operation vars. Only performing it once.
mDirectOperation = null;
}
break;
case POST_LIKE:
// nothing special to do in this case
break;
}
} else {
mCommentId = 0;
}
}
private ReaderPost getPost() {
return mPost;
}
private void showProgress() {
ProgressBar progress = (ProgressBar) findViewById(R.id.progress_loading);
if (progress != null) {
progress.setVisibility(View.VISIBLE);
}
}
private void hideProgress() {
ProgressBar progress = (ProgressBar) findViewById(R.id.progress_loading);
if (progress != null) {
progress.setVisibility(View.GONE);
}
}
@SuppressWarnings("unused")
public void onEventMainThread(ReaderEvents.UpdateCommentsStarted event) {
mIsUpdatingComments = true;
}
@SuppressWarnings("unused")
public void onEventMainThread(ReaderEvents.UpdateCommentsEnded event) {
if (isFinishing()) return;
mIsUpdatingComments = false;
mHasUpdatedComments = true;
hideProgress();
if (event.getResult().isNewOrChanged()) {
mRestorePosition = getCurrentPosition();
refreshComments();
} else {
checkEmptyView();
}
setRefreshing(false);
}
/*
* request comments for this post
*/
private void updateComments(boolean showProgress, boolean requestNextPage) {
if (mIsUpdatingComments) {
AppLog.w(T.READER, "reader comments > already updating comments");
setRefreshing(false);
return;
}
if (!NetworkUtils.isNetworkAvailable(this)) {
AppLog.w(T.READER, "reader comments > no connection, update canceled");
setRefreshing(false);
return;
}
if (showProgress) {
showProgress();
}
ReaderCommentService.startService(this, mPost.blogId, mPost.postId, requestNextPage);
}
private void checkEmptyView() {
TextView txtEmpty = (TextView) findViewById(R.id.text_empty);
if (txtEmpty == null) return;
boolean isEmpty = hasCommentAdapter()
&& getCommentAdapter().isEmpty()
&& !mIsSubmittingComment;
if (isEmpty && !NetworkUtils.isNetworkAvailable(this)) {
txtEmpty.setText(R.string.no_network_message);
txtEmpty.setVisibility(View.VISIBLE);
} else if (isEmpty && mHasUpdatedComments) {
txtEmpty.setText(R.string.reader_empty_comments);
txtEmpty.setVisibility(View.VISIBLE);
} else {
txtEmpty.setVisibility(View.GONE);
}
}
/*
* refresh adapter so latest comments appear
*/
private void refreshComments() {
AppLog.d(T.READER, "reader comments > refreshComments");
getCommentAdapter().refreshComments();
}
/*
* scrolls the passed comment to the top of the listView
*/
private void scrollToCommentId(long commentId) {
int position = getCommentAdapter().positionOfCommentId(commentId);
if (position > -1) {
mRecyclerView.scrollToPosition(position);
}
}
/*
* Smoothly scrolls the passed comment to the top of the listView
*/
private void smoothScrollToCommentId(long commentId) {
int position = getCommentAdapter().positionOfCommentId(commentId);
if (position > -1) {
mRecyclerView.smoothScrollToPosition(position);
}
}
/*
* submit the text typed into the comment box as a comment on the current post
*/
private void submitComment() {
final String commentText = EditTextUtils.getText(mEditComment);
if (TextUtils.isEmpty(commentText)) {
return;
}
if (!NetworkUtils.checkConnection(this)) {
return;
}
AnalyticsUtils.trackWithReaderPostDetails(
AnalyticsTracker.Stat.READER_ARTICLE_COMMENTED_ON, mPost);
mSubmitReplyBtn.setEnabled(false);
mEditComment.setEnabled(false);
mIsSubmittingComment = true;
// generate a "fake" comment id to assign to the new comment so we can add it to the db
// and reflect it in the adapter before the API call returns
final long fakeCommentId = ReaderCommentActions.generateFakeCommentId();
ReaderActions.CommentActionListener actionListener = new ReaderActions.CommentActionListener() {
@Override
public void onActionResult(boolean succeeded, ReaderComment newComment) {
if (isFinishing()) {
return;
}
mIsSubmittingComment = false;
mSubmitReplyBtn.setEnabled(true);
mEditComment.setEnabled(true);
if (succeeded) {
// stop highlighting the fake comment and replace it with the real one
getCommentAdapter().setHighlightCommentId(0, false);
getCommentAdapter().replaceComment(fakeCommentId, newComment);
setReplyToCommentId(0, false);
mEditComment.getAutoSaveTextHelper().clearSavedText(mEditComment);
} else {
mEditComment.setText(commentText);
getCommentAdapter().removeComment(fakeCommentId);
ToastUtils.showToast(
ReaderCommentListActivity.this, R.string.reader_toast_err_comment_failed, ToastUtils.Duration.LONG);
}
checkEmptyView();
}
};
long wpComUserId = mAccountStore.getAccount().getUserId();
ReaderComment newComment = ReaderCommentActions.submitPostComment(
getPost(),
fakeCommentId,
commentText,
mReplyToCommentId,
actionListener,
wpComUserId);
if (newComment != null) {
mEditComment.setText(null);
// add the "fake" comment to the adapter, highlight it, and show a progress bar
// next to it while it's submitted
getCommentAdapter().setHighlightCommentId(newComment.commentId, true);
getCommentAdapter().addComment(newComment);
// make sure it's scrolled into view
scrollToCommentId(fakeCommentId);
checkEmptyView();
}
}
private int getCurrentPosition() {
if (mRecyclerView != null && hasCommentAdapter()) {
return ((LinearLayoutManager) mRecyclerView.getLayoutManager()).findFirstVisibleItemPosition();
} else {
return 0;
}
}
private void setRefreshing(boolean refreshing) {
mSwipeToRefreshHelper.setRefreshing(refreshing);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RequestCodes.DO_LOGIN && resultCode == Activity.RESULT_OK) {
mBackFromLogin = true;
}
}
}