package org.wordpress.android.ui.notifications; import android.app.Activity; import android.app.Fragment; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.support.design.widget.AppBarLayout; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.widget.RadioGroup; import android.widget.TextView; import com.android.volley.VolleyError; import com.wordpress.rest.RestRequest; import org.json.JSONObject; import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.datasets.NotificationsTable; import org.wordpress.android.fluxc.model.CommentStatus; import org.wordpress.android.fluxc.model.SiteModel; import org.wordpress.android.fluxc.store.AccountStore; import org.wordpress.android.models.Note; import org.wordpress.android.push.GCMMessageService; import org.wordpress.android.ui.ActivityLauncher; import org.wordpress.android.ui.RequestCodes; import org.wordpress.android.ui.main.WPMainActivity; import org.wordpress.android.ui.notifications.adapters.NotesAdapter; import org.wordpress.android.ui.notifications.services.NotificationsUpdateService; import org.wordpress.android.ui.notifications.utils.NotificationsActions; import org.wordpress.android.util.AniUtils; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.ToastUtils.Duration; import javax.inject.Inject; import de.greenrobot.event.EventBus; import static android.app.Activity.RESULT_OK; public class NotificationsListFragment extends Fragment implements WPMainActivity.OnScrollToTopListener, RadioGroup.OnCheckedChangeListener, NotesAdapter.DataLoadedListener { public static final String NOTE_ID_EXTRA = "noteId"; public static final String NOTE_INSTANT_REPLY_EXTRA = "instantReply"; public static final String NOTE_PREFILLED_REPLY_EXTRA = "prefilledReplyText"; public static final String NOTE_MODERATE_ID_EXTRA = "moderateNoteId"; public static final String NOTE_MODERATE_STATUS_EXTRA = "moderateNoteStatus"; public static final String NOTE_CURRENT_LIST_FILTER_EXTRA = "currentFilter"; private static final String KEY_LIST_SCROLL_POSITION = "scrollPosition"; private NotesAdapter mNotesAdapter; private SwipeRefreshLayout mSwipeRefreshLayout; private LinearLayoutManager mLinearLayoutManager; private RecyclerView mRecyclerView; private ViewGroup mEmptyView; private View mFilterView; private RadioGroup mFilterRadioGroup; private View mFilterDivider; private View mNewNotificationsBar; private long mRestoredScrollNoteID; private boolean mIsAnimatingOutNewNotificationsBar; @Inject AccountStore mAccountStore; public static NotificationsListFragment newInstance() { return new NotificationsListFragment(); } /** * For responding to tapping of notes */ public interface OnNoteClickListener { void onClickNote(String noteId); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((WordPress) getActivity().getApplication()).component().inject(this); } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.notifications_fragment_notes_list, container, false); mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view_notes); mFilterRadioGroup = (RadioGroup) view.findViewById(R.id.notifications_radio_group); mFilterRadioGroup.setOnCheckedChangeListener(this); mFilterDivider = view.findViewById(R.id.notifications_filter_divider); mEmptyView = (ViewGroup) view.findViewById(R.id.empty_view); mFilterView = view.findViewById(R.id.notifications_filter); mLinearLayoutManager = new LinearLayoutManager(getActivity()); mRecyclerView.setLayoutManager(mLinearLayoutManager); mSwipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipe_refresh_notifications); mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { hideNewNotificationsBar(); fetchNotesFromRemote(); } }); // bar that appears at bottom after new notes are received and the user is on this screen mNewNotificationsBar = view.findViewById(R.id.layout_new_notificatons); mNewNotificationsBar.setVisibility(View.GONE); mNewNotificationsBar.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { onScrollToTop(); } }); return view; } /* * scroll listener assigned to the recycler when the "new notifications" ribbon is shown to hide * it upon scrolling */ private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); mRecyclerView.removeOnScrollListener(this); // remove the listener now clearPendingNotificationsItemsOnUI(); } }; private void clearPendingNotificationsItemsOnUI() { hideNewNotificationsBar(); // Immediately update the unseen ribbon EventBus.getDefault().post(new NotificationEvents.NotificationsUnseenStatus( false )); // Then hit the server NotificationsActions.updateNotesSeenTimestamp(); // Removes app notifications from the system bar new Thread(new Runnable() { public void run() { GCMMessageService.removeAllNotifications(getActivity()); } }).start(); } @Override public void onScrollToTop() { if(!isAdded()) return; clearPendingNotificationsItemsOnUI(); if (getFirstVisibleItemID() > 0) { mLinearLayoutManager.smoothScrollToPosition(mRecyclerView, null, 0); } } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mRecyclerView.setAdapter(getNotesAdapter()); if (savedInstanceState != null) { setRestoredFirstVisibleItemID(savedInstanceState.getLong(KEY_LIST_SCROLL_POSITION, 0)); } } private void updateNote(String noteId, CommentStatus status) { Note note = NotificationsTable.getNoteById(noteId); if (note == null) return; note.setLocalStatus(status.toString()); NotificationsTable.saveNote(note); EventBus.getDefault().post(new NotificationEvents.NotificationsChanged()); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { String noteId = data.getStringExtra(NOTE_MODERATE_ID_EXTRA); String newStatus = data.getStringExtra(NOTE_MODERATE_STATUS_EXTRA); if (!TextUtils.isEmpty(noteId) && !TextUtils.isEmpty(newStatus)) { updateNote(noteId, CommentStatus.fromString(newStatus)); } } } @Override public void onResume() { super.onResume(); hideNewNotificationsBar(); // Immediately update the unseen ribbon EventBus.getDefault().post(new NotificationEvents.NotificationsUnseenStatus( false )); if (!mAccountStore.hasAccessToken()) { // let user know that notifications require a wp.com account and enable sign-in showEmptyView(R.string.notifications_account_required, 0, R.string.sign_in); mFilterRadioGroup.setVisibility(View.GONE); mSwipeRefreshLayout.setVisibility(View.GONE); } else { getNotesAdapter().reloadNotesFromDBAsync(); } } @Override public void onDataLoaded(int itemsCount) { if (itemsCount > 0) { hideEmptyView(); if (mRestoredScrollNoteID > 0) { restoreListScrollPosition(); } } else { showEmptyViewForCurrentFilter(); } } private NotesAdapter getNotesAdapter() { if (mNotesAdapter == null) { mNotesAdapter = new NotesAdapter(getActivity(), this, null); mNotesAdapter.setOnNoteClickListener(mOnNoteClickListener); } return mNotesAdapter; } private final OnNoteClickListener mOnNoteClickListener = new OnNoteClickListener() { @Override public void onClickNote(String noteId) { if (!isAdded()) { return; } if (TextUtils.isEmpty(noteId)) return; // open the latest version of this note just in case it has changed - this can // happen if the note was tapped from the list fragment after it was updated // by another fragment (such as NotificationCommentLikeFragment) openNoteForReply(getActivity(), noteId, false, null, mNotesAdapter.getCurrentFilter()); } }; private static Intent getOpenNoteIntent(Activity activity, String noteId) { Intent detailIntent = new Intent(activity, NotificationsDetailActivity.class); detailIntent.putExtra(NOTE_ID_EXTRA, noteId); return detailIntent; } /** * Open a note fragment based on the type of note */ public static void openNoteForReply(Activity activity, String noteId, boolean shouldShowKeyboard, String replyText, NotesAdapter.FILTERS filter) { if (noteId == null || activity == null) { return; } if (activity.isFinishing()) { return; } Intent detailIntent = getOpenNoteIntent(activity, noteId); detailIntent.putExtra(NOTE_INSTANT_REPLY_EXTRA, shouldShowKeyboard); if (!TextUtils.isEmpty(replyText)) { detailIntent.putExtra(NOTE_PREFILLED_REPLY_EXTRA, replyText); } detailIntent.putExtra(NOTE_CURRENT_LIST_FILTER_EXTRA, filter); openNoteForReplyWithParams(detailIntent, activity); } private static void openNoteForReplyWithParams(Intent detailIntent, Activity activity) { activity.startActivityForResult(detailIntent, RequestCodes.NOTE_DETAIL); } private void setNoteIsHidden(String noteId, boolean isHidden) { if (mNotesAdapter == null) return; if (isHidden) { mNotesAdapter.addHiddenNoteId(noteId); } else { // Scroll the row into view if it isn't visible so the animation can be seen int notePosition = mNotesAdapter.getPositionForNote(noteId); if (notePosition != RecyclerView.NO_POSITION && mLinearLayoutManager.findFirstCompletelyVisibleItemPosition() > notePosition) { mLinearLayoutManager.scrollToPosition(notePosition); } mNotesAdapter.removeHiddenNoteId(noteId); } } private void setNoteIsModerating(String noteId, boolean isModerating) { if (mNotesAdapter == null) return; if (isModerating) { mNotesAdapter.addModeratingNoteId(noteId); } else { mNotesAdapter.removeModeratingNoteId(noteId); } } private void showEmptyView(@StringRes int titleResId) { showEmptyView(titleResId, 0, 0); } private void showEmptyView(@StringRes int titleResId, @StringRes int descriptionResId, @StringRes int buttonResId) { if (isAdded() && mEmptyView != null) { mEmptyView.setVisibility(View.VISIBLE); mFilterDivider.setVisibility(View.GONE); mRecyclerView.setVisibility(View.GONE); setFilterViewScrollable(false); ((TextView) mEmptyView.findViewById(R.id.text_empty)).setText(titleResId); TextView descriptionTextView = (TextView) mEmptyView.findViewById(R.id.text_empty_description); if (descriptionResId > 0) { descriptionTextView.setText(descriptionResId); } else { descriptionTextView.setVisibility(View.GONE); } TextView btnAction = (TextView) mEmptyView.findViewById(R.id.button_empty_action); if (buttonResId > 0) { btnAction.setText(buttonResId); btnAction.setVisibility(View.VISIBLE); } else { btnAction.setVisibility(View.GONE); } btnAction.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { performActionForActiveFilter(); } }); } } private void setFilterViewScrollable(boolean isScrollable) { if (mFilterView != null && mFilterView.getLayoutParams() instanceof AppBarLayout.LayoutParams) { AppBarLayout.LayoutParams params = (AppBarLayout.LayoutParams) mFilterView.getLayoutParams(); if (isScrollable) { params.setScrollFlags( AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS ); } else { params.setScrollFlags(0); } } } private void hideEmptyView() { if (isAdded() && mEmptyView != null) { setFilterViewScrollable(true); mEmptyView.setVisibility(View.GONE); mFilterDivider.setVisibility(View.VISIBLE); mRecyclerView.setVisibility(View.VISIBLE); mSwipeRefreshLayout.setVisibility(View.VISIBLE); } } private void fetchNotesFromRemote() { if (!isAdded() || mNotesAdapter == null) { return; } if (!NetworkUtils.isNetworkAvailable(getActivity())) { mSwipeRefreshLayout.setRefreshing(false); return; } NotificationsUpdateService.startService(getActivity()); } // Show different empty list message and action button based on the active filter private void showEmptyViewForCurrentFilter() { if (!mAccountStore.hasAccessToken()) return; int i = mFilterRadioGroup.getCheckedRadioButtonId(); if (i == R.id.notifications_filter_all) { showEmptyView( R.string.notifications_empty_all, R.string.notifications_empty_action_all, R.string.notifications_empty_view_reader ); } else if (i == R.id.notifications_filter_unread) {// User might not have a blog, if so just show the title if (getSelectedSite() == null) { showEmptyView(R.string.notifications_empty_unread); } else { showEmptyView( R.string.notifications_empty_unread, R.string.notifications_empty_action_unread, R.string.new_post ); } } else if (i == R.id.notifications_filter_comments) { showEmptyView( R.string.notifications_empty_comments, R.string.notifications_empty_action_comments, R.string.notifications_empty_view_reader ); } else if (i == R.id.notifications_filter_follows) { showEmptyView( R.string.notifications_empty_followers, R.string.notifications_empty_action_followers_likes, R.string.notifications_empty_view_reader ); } else if (i == R.id.notifications_filter_likes) { showEmptyView( R.string.notifications_empty_likes, R.string.notifications_empty_action_followers_likes, R.string.notifications_empty_view_reader ); } else { showEmptyView(R.string.notifications_empty_list); } } private void performActionForActiveFilter() { if (mFilterRadioGroup == null || !isAdded()) return; if (!mAccountStore.hasAccessToken()) { ActivityLauncher.showSignInForResult(getActivity()); return; } int i = mFilterRadioGroup.getCheckedRadioButtonId(); if (i == R.id.notifications_filter_unread) { // Create a new post ActivityLauncher.addNewPostOrPageForResult(getActivity(), getSelectedSite(), false); } else {// Switch to Reader tab if (getActivity() instanceof WPMainActivity) { ((WPMainActivity) getActivity()).setReaderTabActive(); } } } private void restoreListScrollPosition() { if (!isAdded() || mRestoredScrollNoteID <= 0) { return; } final int pos = getNotesAdapter().getPositionForNote(String.valueOf(mRestoredScrollNoteID)); if (pos != RecyclerView.NO_POSITION && pos < mNotesAdapter.getItemCount()) { // Restore scroll position in list mLinearLayoutManager.scrollToPosition(pos); mRestoredScrollNoteID = 0L; } } @Override public void onSaveInstanceState(@NonNull Bundle outState) { if (outState.isEmpty()) { outState.putBoolean("bug_19917_fix", true); } // Save list view scroll position outState.putLong(KEY_LIST_SCROLL_POSITION, getFirstVisibleItemID()); super.onSaveInstanceState(outState); } private long getFirstVisibleItemID() { if (!isAdded() || mRecyclerView == null) { return RecyclerView.NO_POSITION; } int pos = mLinearLayoutManager.findFirstCompletelyVisibleItemPosition(); return getNotesAdapter().getItemId(pos); } private void setRestoredFirstVisibleItemID(long noteID) { mRestoredScrollNoteID = noteID; } // Notification filter methods @Override public void onCheckedChanged(RadioGroup radioGroup, int checkedId) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { // Filter the list according to the RadioGroup selection int checkedId = mFilterRadioGroup.getCheckedRadioButtonId(); if (checkedId == R.id.notifications_filter_all) { mNotesAdapter.setFilter(NotesAdapter.FILTERS.FILTER_ALL); } else if (checkedId == R.id.notifications_filter_unread) { mNotesAdapter.setFilter(NotesAdapter.FILTERS.FILTER_UNREAD); } else if (checkedId == R.id.notifications_filter_comments) { mNotesAdapter.setFilter(NotesAdapter.FILTERS.FILTER_COMMENT); } else if (checkedId == R.id.notifications_filter_follows) { mNotesAdapter.setFilter(NotesAdapter.FILTERS.FILTER_FOLLOW); } else if (checkedId == R.id.notifications_filter_likes) { mNotesAdapter.setFilter(NotesAdapter.FILTERS.FILTER_LIKE); } else { mNotesAdapter.setFilter(NotesAdapter.FILTERS.FILTER_ALL); } restoreListScrollPosition(); } }); } @Override public void onStop() { EventBus.getDefault().unregister(this); super.onStop(); } @Override public void onStart() { super.onStart(); EventBus.getDefault().registerSticky(this); } @SuppressWarnings("unused") public void onEventMainThread(NotificationEvents.NotificationsRefreshError error) { if (isAdded()) { ToastUtils.showToast(getActivity(), getString(R.string.error_refresh_notifications)); mSwipeRefreshLayout.setRefreshing(false); } } @SuppressWarnings("unused") public void onEventMainThread(final NotificationEvents.NotificationsRefreshCompleted event) { if (!isAdded()) { return; } mSwipeRefreshLayout.setRefreshing(false); mNotesAdapter.addAll(event.notes, true); } @SuppressWarnings("unused") public void onEventMainThread(final NotificationEvents.NoteModerationStatusChanged event) { if (event.isModerating) { setNoteIsModerating(event.noteId, event.isModerating); EventBus.getDefault().removeStickyEvent(NotificationEvents.NoteModerationStatusChanged.class); } else { // Moderation done -> refresh the note before calling the end. NotificationsActions.downloadNoteAndUpdateDB(event.noteId, new RestRequest.Listener() { @Override public void onResponse(JSONObject response) { setNoteIsModerating(event.noteId, event.isModerating); EventBus.getDefault().removeStickyEvent(NotificationEvents.NoteModerationStatusChanged.class); } }, new RestRequest.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { setNoteIsModerating(event.noteId, event.isModerating); EventBus.getDefault().removeStickyEvent(NotificationEvents.NoteModerationStatusChanged.class); } } ); } } @SuppressWarnings("unused") public void onEventMainThread(final NotificationEvents.NoteLikeStatusChanged event) { // Like/unlike done -> refresh the note and update db NotificationsActions.downloadNoteAndUpdateDB(event.noteId, new RestRequest.Listener() { @Override public void onResponse(JSONObject response) { EventBus.getDefault().removeStickyEvent(NotificationEvents.NoteLikeStatusChanged.class); //now re-set the object in our list adapter with the note saved in the updated DB Note note = NotificationsTable.getNoteById(event.noteId); if (note != null) { mNotesAdapter.replaceNote(note); } } }, new RestRequest.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { EventBus.getDefault().removeStickyEvent(NotificationEvents.NoteLikeStatusChanged.class); } } ); } @SuppressWarnings("unused") public void onEventMainThread(NotificationEvents.NoteVisibilityChanged event) { setNoteIsHidden(event.noteId, event.isHidden); EventBus.getDefault().removeStickyEvent(NotificationEvents.NoteVisibilityChanged.class); } @SuppressWarnings("unused") public void onEventMainThread(NotificationEvents.NoteModerationFailed event) { if (isAdded()) { ToastUtils.showToast(getActivity(), R.string.error_moderate_comment, Duration.LONG); } EventBus.getDefault().removeStickyEvent(NotificationEvents.NoteModerationFailed.class); } public SiteModel getSelectedSite() { if (getActivity() instanceof WPMainActivity) { WPMainActivity mainActivity = (WPMainActivity) getActivity(); return mainActivity.getSelectedSite(); } return null; } @SuppressWarnings("unused") public void onEventMainThread(NotificationEvents.NotificationsChanged event) { if (!isAdded()) { return; } mRestoredScrollNoteID = getFirstVisibleItemID(); // Remember the ID of the first note visible on the screen getNotesAdapter().reloadNotesFromDBAsync(); if (event.hasUnseenNotes) { showNewUnseenNotificationsUI(); } } @SuppressWarnings("unused") public void onEventMainThread(NotificationEvents.NotificationsUnseenStatus event) { if (!isAdded()) { return; } // if a new note arrives when the notifications list is on Foreground. if (event.hasUnseenNotes) { showNewUnseenNotificationsUI(); } else { hideNewNotificationsBar(); } } private void showNewUnseenNotificationsUI() { if (!isAdded()) return; // Make sure the RecyclerView is configured if (mRecyclerView == null || mRecyclerView.getLayoutManager() == null) { return; } mRecyclerView.clearOnScrollListeners(); // Just one listener. Multiple notes received here add multiple listeners. // Assign the scroll listener to hide the bar when the recycler is scrolled, but don't assign // it right away since the user may be scrolling when the bar appears (which would cause it // to disappear as soon as it's displayed) mRecyclerView.postDelayed(new Runnable() { @Override public void run() { if (isAdded()) { mRecyclerView.addOnScrollListener(mOnScrollListener); } } }, 1000L); // Check if the first item is visible on the screen View child = mRecyclerView.getLayoutManager().getChildAt(0); if (child != null && mRecyclerView.getLayoutManager().getPosition(child) > 0) { showNewNotificationsBar(); } } /* * bar that appears at the bottom when new notifications are available */ private boolean isNewNotificationsBarShowing() { return (mNewNotificationsBar != null && mNewNotificationsBar.getVisibility() == View.VISIBLE); } private void showNewNotificationsBar() { if (!isAdded() || isNewNotificationsBarShowing()) { return; } AniUtils.startAnimation(mNewNotificationsBar, R.anim.notifications_bottom_bar_in); mNewNotificationsBar.setVisibility(View.VISIBLE); } private void hideNewNotificationsBar() { if (!isAdded() || !isNewNotificationsBarShowing() || mIsAnimatingOutNewNotificationsBar) { return; } mIsAnimatingOutNewNotificationsBar = true; Animation.AnimationListener listener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { if (isAdded()) { mNewNotificationsBar.setVisibility(View.GONE); mIsAnimatingOutNewNotificationsBar = false; } } @Override public void onAnimationRepeat(Animation animation) { } }; AniUtils.startAnimation(mNewNotificationsBar, R.anim.notifications_bottom_bar_out, listener); } }