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.Bundle;
import android.support.annotation.StringRes;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.text.TextUtils;
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.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.wordpress.android.R;
import org.wordpress.android.WordPress;
import org.wordpress.android.fluxc.Dispatcher;
import org.wordpress.android.fluxc.action.CommentAction;
import org.wordpress.android.fluxc.generated.CommentActionBuilder;
import org.wordpress.android.fluxc.model.CommentModel;
import org.wordpress.android.fluxc.model.CommentStatus;
import org.wordpress.android.fluxc.model.SiteModel;
import org.wordpress.android.fluxc.store.CommentStore;
import org.wordpress.android.fluxc.store.CommentStore.FetchCommentsPayload;
import org.wordpress.android.fluxc.store.CommentStore.OnCommentChanged;
import org.wordpress.android.fluxc.store.CommentStore.RemoteCommentPayload;
import org.wordpress.android.models.CommentList;
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 java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
public class CommentsListFragment extends Fragment {
public static final int COMMENTS_PER_PAGE = 30;
interface OnCommentSelectedListener {
void onCommentSelected(long commentId);
}
public enum CommentStatusCriteria implements FilterCriteria {
ALL(R.string.comment_status_all),
UNAPPROVED(R.string.comment_status_unapproved),
APPROVED(R.string.comment_status_approved),
TRASH(R.string.comment_status_trash),
SPAM(R.string.comment_status_spam),
DELETE(R.string.comment_status_trash);
private final int mLabelResId;
CommentStatusCriteria(@StringRes int labelResId) {
mLabelResId = labelResId;
}
@Override
public String getLabel() {
return WordPress.getContext().getString(mLabelResId);
}
public static CommentStatusCriteria fromCommentStatus(CommentStatus status) {
return valueOf(status.name());
}
public CommentStatus toCommentStatus() {
return CommentStatus.fromString(name());
}
}
private boolean mIsUpdatingComments = false;
private boolean mCanLoadMoreComments = true;
boolean mHasAutoRefreshedComments = false;
private final CommentStatusCriteria[] commentStatuses = {
CommentStatusCriteria.ALL, CommentStatusCriteria.UNAPPROVED, CommentStatusCriteria.APPROVED,
CommentStatusCriteria.TRASH, CommentStatusCriteria.SPAM};
private EmptyViewMessageType mEmptyViewMessageType = EmptyViewMessageType.NO_CONTENT;
private FilteredRecyclerView mFilteredCommentsView;
private CommentAdapter mAdapter;
private ActionMode mActionMode;
private CommentStatusCriteria mCommentStatusFilter = CommentStatusCriteria.ALL;
private SiteModel mSite;
@Inject Dispatcher mDispatcher;
@Inject CommentStore mCommentStore;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((WordPress) getActivity().getApplication()).component().inject(this);
mDispatcher.register(this);
updateSiteOrFinishActivity(savedInstanceState);
}
@Override
public void onDestroy() {
mDispatcher.unregister(this);
super.onDestroy();
}
private void updateSiteOrFinishActivity(Bundle savedInstanceState) {
if (savedInstanceState == null) {
if (getArguments() != null) {
mSite = (SiteModel) getArguments().getSerializable(WordPress.SITE);
} else {
mSite = (SiteModel) getActivity().getIntent().getSerializableExtra(WordPress.SITE);
}
} else {
mSite = (SiteModel) savedInstanceState.getSerializable(WordPress.SITE);
}
if (mSite == null) {
ToastUtils.showToast(getActivity(), R.string.blog_not_found, ToastUtils.Duration.SHORT);
getActivity().finish();
}
}
private CommentAdapter getAdapter() {
if (mAdapter == null) {
// called after comments have been loaded
CommentAdapter.OnDataLoadedListener dataLoadedListener = new CommentAdapter.OnDataLoadedListener() {
@Override
public void onDataLoaded(boolean isEmpty) {
if (!isAdded()) return;
if (!isEmpty) {
// 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);
}
}
};
// adapter calls this to request more comments from server when it reaches the end
CommentAdapter.OnLoadMoreListener loadMoreListener = new CommentAdapter.OnLoadMoreListener() {
@Override
public void onLoadMore() {
if (mCanLoadMoreComments && !mIsUpdatingComments) {
updateComments(true);
}
}
};
// adapter calls this when selected comments have changed (CAB)
CommentAdapter.OnSelectedItemsChangeListener changeListener = new CommentAdapter.OnSelectedItemsChangeListener() {
@Override
public void onSelectedItemsChanged() {
if (mActionMode != null) {
if (getSelectedCommentCount() == 0) {
mActionMode.finish();
} else {
updateActionModeTitle();
// must invalidate to ensure onPrepareActionMode is called
mActionMode.invalidate();
}
}
}
};
CommentAdapter.OnCommentPressedListener pressedListener = new CommentAdapter.OnCommentPressedListener() {
@Override
public void onCommentPressed(int position, View view) {
CommentModel comment = getAdapter().getItem(position);
if (comment == null) {
return;
}
if (mActionMode == null) {
mFilteredCommentsView.invalidate();
if (getActivity() instanceof OnCommentSelectedListener) {
((OnCommentSelectedListener) getActivity()).onCommentSelected(comment.getRemoteCommentId());
}
} else {
getAdapter().toggleItemSelected(position, view);
}
}
@Override
public void onCommentLongPressed(int position, View view) {
// 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);
}
}
};
mAdapter = new CommentAdapter(getActivity(), mSite);
mAdapter.setOnCommentPressedListener(pressedListener);
mAdapter.setOnDataLoadedListener(dataLoadedListener);
mAdapter.setOnLoadMoreListener(loadMoreListener);
mAdapter.setOnSelectedItemsChangeListener(changeListener);
}
return mAdapter;
}
private boolean hasAdapter() {
return (mAdapter != null);
}
private int getSelectedCommentCount() {
return getAdapter().getSelectedCommentCount();
}
public void removeComment(CommentModel comment) {
if (hasAdapter() && comment != null) {
getAdapter().removeComment(comment);
}
// Show the empty view if the comment count drop to zero
updateEmptyView();
}
@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 (!NetworkUtils.isNetworkAvailable(getActivity())) {
mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.NETWORK_ERROR);
return;
}
// Restore the empty view's message
mFilteredCommentsView.updateEmptyView(mEmptyViewMessageType);
if (!mHasAutoRefreshedComments) {
updateComments(false);
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) {
AppPrefs.setCommentsStatusFilter((CommentStatusCriteria) criteria);
mCommentStatusFilter = (CommentStatusCriteria) criteria;
}
@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 || filter == CommentStatusCriteria.ALL) {
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.ic_dropdown_blue_light_24dp);
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.toCommentStatus());
}
}
public void setCommentStatusFilter(CommentStatus statusFilter) {
mCommentStatusFilter = CommentStatusCriteria.fromCommentStatus(statusFilter);
}
private void dismissDialog(int id) {
if (!isAdded())
return;
try {
getActivity().dismissDialog(id);
} catch (IllegalArgumentException e) {
// raised when dialog wasn't created
}
}
private void moderateSelectedComments(final CommentStatus newStatus) {
final CommentList selectedComments = getAdapter().getSelectedComments();
final CommentList updateComments = new CommentList();
// build list of comments whose status is different than passed
for (CommentModel comment : selectedComments) {
if (CommentStatus.fromString(comment.getStatus()) != newStatus) {
updateComments.add(comment);
}
}
if (updateComments.size() == 0) return;
if (!NetworkUtils.checkConnection(getActivity())) return;
getAdapter().clearSelectedComments();
finishActionMode();
moderateComments(updateComments, newStatus);
}
private void confirmDeleteComments() {
if (mCommentStatusFilter == CommentStatusCriteria.TRASH) {
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 int dlgId = deletePermanently ? CommentDialogs.ID_COMMENT_DLG_DELETING
: CommentDialogs.ID_COMMENT_DLG_TRASHING;
final CommentList selectedComments = getAdapter().getSelectedComments();
CommentStatus newStatus = CommentStatus.TRASH;
if (deletePermanently) {
newStatus = CommentStatus.DELETED;
}
dismissDialog(dlgId);
finishActionMode();
moderateComments(selectedComments, newStatus);
}
private boolean shouldRemoveCommentFromList(CommentModel comment) {
CommentStatus status = CommentStatus.fromString(comment.getStatus());
switch (mCommentStatusFilter) {
case ALL:
return status != CommentStatus.APPROVED && status != CommentStatus.UNAPPROVED;
case UNAPPROVED:
return status != CommentStatus.UNAPPROVED;
case APPROVED:
return status != CommentStatus.APPROVED;
case TRASH:
return status != CommentStatus.TRASH;
case SPAM:
return status != CommentStatus.SPAM;
case DELETE:
default:
return true;
}
}
private void moderateComments(CommentList comments, CommentStatus status) {
for (CommentModel comment: comments) {
// Preemptive update
comment.setStatus(status.toString());
if (shouldRemoveCommentFromList(comment)) {
removeComment(comment);
}
if (status == CommentStatus.DELETED) {
// For deletion, we need to dispatch a specific action.
mDispatcher.dispatch(CommentActionBuilder.newDeleteCommentAction(
new RemoteCommentPayload(mSite, comment)));
} else {
// Dispatch the update
mDispatcher.dispatch(CommentActionBuilder.newPushCommentAction(
new RemoteCommentPayload(mSite, comment)));
}
}
}
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.toCommentStatus());
}
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.toCommentStatus());
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.toCommentStatus());
}
mFilteredCommentsView.updateEmptyView(EmptyViewMessageType.LOADING);
int offset = 0;
if (loadMore) {
offset = getAdapter().getItemCount();
mFilteredCommentsView.showLoadingProgress();
}
mFilteredCommentsView.setRefreshing(true);
mDispatcher.dispatch(CommentActionBuilder.newFetchCommentsAction(new FetchCommentsPayload(mSite,
mCommentStatusFilter.toCommentStatus(), COMMENTS_PER_PAGE, offset)));
}
public String getEmptyViewMessage() {
return mEmptyViewMessageType.name();
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putSerializable(WordPress.SITE, mSite);
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, boolean isVisible) {
final MenuItem item = menu.findItem(menuId);
if (item == null || (item.isEnabled() == isEnabled && item.isVisible() == isVisible))
return;
item.setVisible(isVisible);
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, true);
setItemEnabled(menu, R.id.menu_unapprove, hasApproved, true);
setItemEnabled(menu, R.id.menu_spam, hasAnyNonSpam, hasAnyNonSpam);
setItemEnabled(menu, R.id.menu_unspam, hasSpam && !hasAnyNonSpam, hasSpam && !hasAnyNonSpam);
setItemEnabled(menu, R.id.menu_trash, hasSelection, true);
final MenuItem trashItem = menu.findItem(R.id.menu_trash);
if (trashItem != null && mCommentStatusFilter == CommentStatusCriteria.TRASH) {
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_unspam) {
moderateSelectedComments(CommentStatus.APPROVED);
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;
}
}
@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
public void onCommentChanged(OnCommentChanged event) {
mFilteredCommentsView.hideLoadingProgress();
mFilteredCommentsView.setRefreshing(false);
// Don't refresh the list on push, we already updated comments
if (event.causeOfChange != CommentAction.PUSH_COMMENT) {
loadComments();
}
if (event.isError()) {
if (!TextUtils.isEmpty(event.error.message)) {
ToastUtils.showToast(getActivity(), event.error.message);
}
// Reload the comment list in case of an error, we want to revert the UI to the previous state.
loadComments();
}
}
}