diff options
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java')
-rw-r--r-- | WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java | 1612 |
1 files changed, 1612 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java new file mode 100644 index 000000000..69409385e --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java @@ -0,0 +1,1612 @@ +package org.wordpress.android.ui.reader; + +import android.app.Fragment; +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.design.widget.Snackbar; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.widget.ListPopupWindow; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.text.Html; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.AdapterView; +import android.widget.AutoCompleteTextView; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.datasets.ReaderBlogTable; +import org.wordpress.android.datasets.ReaderDatabase; +import org.wordpress.android.datasets.ReaderPostTable; +import org.wordpress.android.datasets.ReaderSearchTable; +import org.wordpress.android.datasets.ReaderTagTable; +import org.wordpress.android.models.FilterCriteria; +import org.wordpress.android.models.ReaderPost; +import org.wordpress.android.models.ReaderPostDiscoverData; +import org.wordpress.android.models.ReaderTag; +import org.wordpress.android.models.ReaderTagList; +import org.wordpress.android.models.ReaderTagType; +import org.wordpress.android.ui.EmptyViewMessageType; +import org.wordpress.android.ui.FilteredRecyclerView; +import org.wordpress.android.ui.main.WPMainActivity; +import org.wordpress.android.ui.prefs.AppPrefs; +import org.wordpress.android.ui.reader.ReaderTypes.ReaderPostListType; +import org.wordpress.android.ui.reader.actions.ReaderActions; +import org.wordpress.android.ui.reader.actions.ReaderBlogActions; +import org.wordpress.android.ui.reader.actions.ReaderBlogActions.BlockedBlogResult; +import org.wordpress.android.ui.reader.adapters.ReaderMenuAdapter; +import org.wordpress.android.ui.reader.adapters.ReaderPostAdapter; +import org.wordpress.android.ui.reader.adapters.ReaderSearchSuggestionAdapter; +import org.wordpress.android.ui.reader.services.ReaderPostService; +import org.wordpress.android.ui.reader.services.ReaderPostService.UpdateAction; +import org.wordpress.android.ui.reader.services.ReaderSearchService; +import org.wordpress.android.ui.reader.services.ReaderUpdateService; +import org.wordpress.android.ui.reader.services.ReaderUpdateService.UpdateTask; +import org.wordpress.android.ui.reader.utils.ReaderUtils; +import org.wordpress.android.ui.reader.views.ReaderSiteHeaderView; +import org.wordpress.android.util.AnalyticsUtils; +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.DisplayUtils; +import org.wordpress.android.util.NetworkUtils; +import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.WPActivityUtils; +import org.wordpress.android.widgets.RecyclerItemDecoration; + +import java.util.ArrayList; +import java.util.Date; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +import de.greenrobot.event.EventBus; + +public class ReaderPostListFragment extends Fragment + implements ReaderInterfaces.OnPostSelectedListener, + ReaderInterfaces.OnTagSelectedListener, + ReaderInterfaces.OnPostPopupListener, + WPMainActivity.OnActivityBackPressedListener, + WPMainActivity.OnScrollToTopListener { + + private ReaderPostAdapter mPostAdapter; + private ReaderSearchSuggestionAdapter mSearchSuggestionAdapter; + + private FilteredRecyclerView mRecyclerView; + private boolean mFirstLoad = true; + private final ReaderTagList mTags = new ReaderTagList(); + + private View mNewPostsBar; + private View mEmptyView; + private View mEmptyViewBoxImages; + private ProgressBar mProgress; + + private SearchView mSearchView; + private MenuItem mSettingsMenuItem; + private MenuItem mSearchMenuItem; + + private ReaderTag mCurrentTag; + private long mCurrentBlogId; + private long mCurrentFeedId; + private String mCurrentSearchQuery; + private ReaderPostListType mPostListType; + + private int mRestorePosition; + + private boolean mIsUpdating; + private boolean mWasPaused; + private boolean mHasUpdatedPosts; + private boolean mIsAnimatingOutNewPostsBar; + + private static boolean mHasPurgedReaderDb; + private static Date mLastAutoUpdateDt; + + private final HistoryStack mTagPreviewHistory = new HistoryStack("tag_preview_history"); + + private static class HistoryStack extends Stack<String> { + private final String keyName; + HistoryStack(@SuppressWarnings("SameParameterValue") String keyName) { + this.keyName = keyName; + } + void restoreInstance(Bundle bundle) { + clear(); + if (bundle.containsKey(keyName)) { + ArrayList<String> history = bundle.getStringArrayList(keyName); + if (history != null) { + this.addAll(history); + } + } + } + void saveInstance(Bundle bundle) { + if (!isEmpty()) { + ArrayList<String> history = new ArrayList<>(); + history.addAll(this); + bundle.putStringArrayList(keyName, history); + } + } + } + + public static ReaderPostListFragment newInstance() { + ReaderTag tag = AppPrefs.getReaderTag(); + if (tag == null) { + tag = ReaderUtils.getDefaultTag(); + } + return newInstanceForTag(tag, ReaderPostListType.TAG_FOLLOWED); + } + + /* + * show posts with a specific tag (either TAG_FOLLOWED or TAG_PREVIEW) + */ + static ReaderPostListFragment newInstanceForTag(ReaderTag tag, ReaderPostListType listType) { + AppLog.d(T.READER, "reader post list > newInstance (tag)"); + + Bundle args = new Bundle(); + args.putSerializable(ReaderConstants.ARG_TAG, tag); + args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, listType); + + ReaderPostListFragment fragment = new ReaderPostListFragment(); + fragment.setArguments(args); + + return fragment; + } + + /* + * show posts in a specific blog + */ + public static ReaderPostListFragment newInstanceForBlog(long blogId) { + AppLog.d(T.READER, "reader post list > newInstance (blog)"); + + Bundle args = new Bundle(); + args.putLong(ReaderConstants.ARG_BLOG_ID, blogId); + args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.BLOG_PREVIEW); + + ReaderPostListFragment fragment = new ReaderPostListFragment(); + fragment.setArguments(args); + + return fragment; + } + + public static ReaderPostListFragment newInstanceForFeed(long feedId) { + AppLog.d(T.READER, "reader post list > newInstance (blog)"); + + Bundle args = new Bundle(); + args.putLong(ReaderConstants.ARG_FEED_ID, feedId); + args.putLong(ReaderConstants.ARG_BLOG_ID, feedId); + args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.BLOG_PREVIEW); + + ReaderPostListFragment fragment = new ReaderPostListFragment(); + fragment.setArguments(args); + + return fragment; + } + + @Override + public void setArguments(Bundle args) { + super.setArguments(args); + + if (args != null) { + if (args.containsKey(ReaderConstants.ARG_TAG)) { + mCurrentTag = (ReaderTag) args.getSerializable(ReaderConstants.ARG_TAG); + } + if (args.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) { + mPostListType = (ReaderPostListType) args.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE); + } + + mCurrentBlogId = args.getLong(ReaderConstants.ARG_BLOG_ID); + mCurrentFeedId = args.getLong(ReaderConstants.ARG_FEED_ID); + mCurrentSearchQuery = args.getString(ReaderConstants.ARG_SEARCH_QUERY); + + if (getPostListType() == ReaderPostListType.TAG_PREVIEW && hasCurrentTag()) { + mTagPreviewHistory.push(getCurrentTagName()); + } + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + AppLog.d(T.READER, "reader post list > restoring instance state"); + if (savedInstanceState.containsKey(ReaderConstants.ARG_TAG)) { + mCurrentTag = (ReaderTag) savedInstanceState.getSerializable(ReaderConstants.ARG_TAG); + } + if (savedInstanceState.containsKey(ReaderConstants.ARG_BLOG_ID)) { + mCurrentBlogId = savedInstanceState.getLong(ReaderConstants.ARG_BLOG_ID); + } + if (savedInstanceState.containsKey(ReaderConstants.ARG_FEED_ID)) { + mCurrentFeedId = savedInstanceState.getLong(ReaderConstants.ARG_FEED_ID); + } + if (savedInstanceState.containsKey(ReaderConstants.ARG_SEARCH_QUERY)) { + mCurrentSearchQuery = savedInstanceState.getString(ReaderConstants.ARG_SEARCH_QUERY); + } + if (savedInstanceState.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) { + mPostListType = (ReaderPostListType) savedInstanceState.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE); + } + if (getPostListType() == ReaderPostListType.TAG_PREVIEW) { + mTagPreviewHistory.restoreInstance(savedInstanceState); + } + mRestorePosition = savedInstanceState.getInt(ReaderConstants.KEY_RESTORE_POSITION); + mWasPaused = savedInstanceState.getBoolean(ReaderConstants.KEY_WAS_PAUSED); + mHasUpdatedPosts = savedInstanceState.getBoolean(ReaderConstants.KEY_ALREADY_UPDATED); + mFirstLoad = savedInstanceState.getBoolean(ReaderConstants.KEY_FIRST_LOAD); + } + } + + @Override + public void onPause() { + super.onPause(); + mWasPaused = true; + } + + @Override + public void onResume() { + super.onResume(); + checkPostAdapter(); + + if (mWasPaused) { + AppLog.d(T.READER, "reader post list > resumed from paused state"); + mWasPaused = false; + if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) { + resumeFollowedTag(); + } else { + refreshPosts(); + } + + // if the user was searching, make sure the filter toolbar is showing + // so the user can see the search keyword they entered + if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) { + mRecyclerView.showToolbar(); + } + } + } + + /* + * called when fragment is resumed and we're looking at posts in a followed tag + */ + private void resumeFollowedTag() { + Object event = EventBus.getDefault().getStickyEvent(ReaderEvents.TagAdded.class); + if (event != null) { + // user just added a tag so switch to it. + String tagName = ((ReaderEvents.TagAdded) event).getTagName(); + EventBus.getDefault().removeStickyEvent(event); + ReaderTag newTag = ReaderUtils.getTagFromTagName(tagName, ReaderTagType.FOLLOWED); + setCurrentTag(newTag); + } else if (!ReaderTagTable.tagExists(getCurrentTag())) { + // current tag no longer exists, revert to default + AppLog.d(T.READER, "reader post list > current tag no longer valid"); + setCurrentTag(ReaderUtils.getDefaultTag()); + } else { + // otherwise, refresh posts to make sure any changes are reflected and auto-update + // posts in the current tag if it's time + refreshPosts(); + updateCurrentTagIfTime(); + } + } + + @Override + public void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + + reloadTags(); + + // purge database and update followed tags/blog if necessary - note that we don't purge unless + // there's a connection to avoid removing posts the user would expect to see offline + if (getPostListType() == ReaderPostListType.TAG_FOLLOWED && NetworkUtils.isNetworkAvailable(getActivity())) { + purgeDatabaseIfNeeded(); + updateFollowedTagsAndBlogsIfNeeded(); + } + } + + @Override + public void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + } + + /* + * ensures the adapter is created and posts are updated if they haven't already been + */ + private void checkPostAdapter() { + if (isAdded() && mRecyclerView.getAdapter() == null) { + mRecyclerView.setAdapter(getPostAdapter()); + + if (!mHasUpdatedPosts && NetworkUtils.isNetworkAvailable(getActivity())) { + mHasUpdatedPosts = true; + if (getPostListType().isTagType()) { + updateCurrentTagIfTime(); + } else if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) { + updatePostsInCurrentBlogOrFeed(UpdateAction.REQUEST_NEWER); + } + } + } + } + + /* + * reset the post adapter to initial state and create it again using the passed list type + */ + private void resetPostAdapter(ReaderPostListType postListType) { + mPostListType = postListType; + mPostAdapter = null; + mRecyclerView.setAdapter(null); + mRecyclerView.setAdapter(getPostAdapter()); + mRecyclerView.setSwipeToRefreshEnabled(isSwipeToRefreshSupported()); + } + + @SuppressWarnings("unused") + public void onEventMainThread(ReaderEvents.FollowedTagsChanged event) { + if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) { + // reload the tag filter since tags have changed + reloadTags(); + + // update the current tag if the list fragment is empty - this will happen if + // the tag table was previously empty (ie: first run) + if (isPostAdapterEmpty()) { + updateCurrentTag(); + } + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(ReaderEvents.FollowedBlogsChanged event) { + // refresh posts if user is viewing "Followed Sites" + if (getPostListType() == ReaderPostListType.TAG_FOLLOWED + && hasCurrentTag() + && getCurrentTag().isFollowedSites()) { + refreshPosts(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + AppLog.d(T.READER, "reader post list > saving instance state"); + + if (mCurrentTag != null) { + outState.putSerializable(ReaderConstants.ARG_TAG, mCurrentTag); + } + if (getPostListType() == ReaderPostListType.TAG_PREVIEW) { + mTagPreviewHistory.saveInstance(outState); + } + + outState.putLong(ReaderConstants.ARG_BLOG_ID, mCurrentBlogId); + outState.putLong(ReaderConstants.ARG_FEED_ID, mCurrentFeedId); + outState.putString(ReaderConstants.ARG_SEARCH_QUERY, mCurrentSearchQuery); + outState.putBoolean(ReaderConstants.KEY_WAS_PAUSED, mWasPaused); + outState.putBoolean(ReaderConstants.KEY_ALREADY_UPDATED, mHasUpdatedPosts); + outState.putBoolean(ReaderConstants.KEY_FIRST_LOAD, mFirstLoad); + outState.putInt(ReaderConstants.KEY_RESTORE_POSITION, getCurrentPosition()); + outState.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, getPostListType()); + + super.onSaveInstanceState(outState); + } + + private int getCurrentPosition() { + if (mRecyclerView != null && hasPostAdapter()) { + return mRecyclerView.getCurrentPosition(); + } else { + return -1; + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.reader_fragment_post_cards, container, false); + mRecyclerView = (FilteredRecyclerView) rootView.findViewById(R.id.reader_recycler_view); + + Context context = container.getContext(); + + // view that appears when current tag/blog has no posts - box images in this view are + // displayed and animated for tags only + mEmptyView = rootView.findViewById(R.id.empty_custom_view); + mEmptyViewBoxImages = mEmptyView.findViewById(R.id.layout_box_images); + + mRecyclerView.setLogT(AppLog.T.READER); + mRecyclerView.setCustomEmptyView(mEmptyView); + mRecyclerView.setFilterListener(new FilteredRecyclerView.FilterListener() { + @Override + public List<FilterCriteria> onLoadFilterCriteriaOptions(boolean refresh) { + return null; + } + + @Override + public void onLoadFilterCriteriaOptionsAsync( + FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener, boolean refresh) { + + loadTags(listener); + } + + @Override + public void onLoadData() { + if (!isAdded()) { + return; + } + if (!NetworkUtils.checkConnection(getActivity())) { + mRecyclerView.setRefreshing(false); + return; + } + + if (mFirstLoad){ + /* let onResume() take care of this logic, as the FilteredRecyclerView.FilterListener onLoadData method + * is called on two moments: once for first time load, and then each time the swipe to refresh gesture + * triggers a refresh + */ + mRecyclerView.setRefreshing(false); + mFirstLoad = false; + } else { + switch (getPostListType()) { + case TAG_FOLLOWED: + case TAG_PREVIEW: + updatePostsWithTag(getCurrentTag(), UpdateAction.REQUEST_NEWER); + break; + case BLOG_PREVIEW: + updatePostsInCurrentBlogOrFeed(UpdateAction.REQUEST_NEWER); + break; + } + // make sure swipe-to-refresh progress shows since this is a manual refresh + mRecyclerView.setRefreshing(true); + } + } + + @Override + public void onFilterSelected(int position, FilterCriteria criteria) { + onTagChanged((ReaderTag)criteria); + } + + @Override + public FilterCriteria onRecallSelection() { + if (hasCurrentTag()) { + return getCurrentTag(); + } else { + AppLog.w(T.READER, "reader post list > no current tag in onRecallSelection"); + return ReaderUtils.getDefaultTag(); + } + } + + @Override + public String onShowEmptyViewMessage(EmptyViewMessageType emptyViewMsgType) { + return null; + } + + @Override + public void onShowCustomEmptyView (EmptyViewMessageType emptyViewMsgType) { + setEmptyTitleAndDescription( + EmptyViewMessageType.NETWORK_ERROR.equals(emptyViewMsgType) + || EmptyViewMessageType.PERMISSION_ERROR.equals(emptyViewMsgType) + || EmptyViewMessageType.GENERIC_ERROR.equals(emptyViewMsgType)); + } + + }); + + // add the item decoration (dividers) to the recycler, skipping the first item if the first + // item is the tag toolbar (shown when viewing posts in followed tags) - this is to avoid + // having the tag toolbar take up more vertical space than necessary + int spacingHorizontal = context.getResources().getDimensionPixelSize(R.dimen.reader_card_margin); + int spacingVertical = context.getResources().getDimensionPixelSize(R.dimen.reader_card_gutters); + mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical, false)); + + // the following will change the look and feel of the toolbar to match the current design + mRecyclerView.setToolbarBackgroundColor(ContextCompat.getColor(context, R.color.blue_medium)); + mRecyclerView.setToolbarSpinnerTextColor(ContextCompat.getColor(context, R.color.white)); + mRecyclerView.setToolbarSpinnerDrawable(R.drawable.arrow); + mRecyclerView.setToolbarLeftAndRightPadding( + getResources().getDimensionPixelSize(R.dimen.margin_medium) + spacingHorizontal, + getResources().getDimensionPixelSize(R.dimen.margin_extra_large) + spacingHorizontal); + + // add a menu to the filtered recycler's toolbar + if (!ReaderUtils.isLoggedOutReader() + && (getPostListType() == ReaderPostListType.TAG_FOLLOWED || getPostListType() == ReaderPostListType.SEARCH_RESULTS)) { + setupRecyclerToolbar(); + } + + mRecyclerView.setSwipeToRefreshEnabled(isSwipeToRefreshSupported()); + + // bar that appears at top after new posts are loaded + mNewPostsBar = rootView.findViewById(R.id.layout_new_posts); + mNewPostsBar.setVisibility(View.GONE); + mNewPostsBar.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mRecyclerView.scrollRecycleViewToPosition(0); + refreshPosts(); + } + }); + + // progress bar that appears when loading more posts + mProgress = (ProgressBar) rootView.findViewById(R.id.progress_footer); + mProgress.setVisibility(View.GONE); + + return rootView; + } + + /* + * adds a menu to the recycler's toolbar containing settings & search items - only called + * for followed tags + */ + private void setupRecyclerToolbar() { + Menu menu = mRecyclerView.addToolbarMenu(R.menu.reader_list); + mSettingsMenuItem = menu.findItem(R.id.menu_reader_settings); + mSearchMenuItem = menu.findItem(R.id.menu_reader_search); + + mSettingsMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + ReaderActivityLauncher.showReaderSubs(getActivity()); + return true; + } + }); + + mSearchView = (SearchView) mSearchMenuItem.getActionView(); + mSearchView.setQueryHint(getString(R.string.reader_hint_post_search)); + mSearchView.setSubmitButtonEnabled(false); + mSearchView.setIconifiedByDefault(true); + mSearchView.setIconified(true); + + // force the search view to take up as much horizontal space as possible (without this + // it looks truncated on landscape) + int maxWidth = DisplayUtils.getDisplayPixelWidth(getActivity()); + mSearchView.setMaxWidth(maxWidth); + + // this is hacky, but we want to change the SearchView's autocomplete to show suggestions + // after a single character is typed, and there's no less hacky way to do this... + View view = mSearchView.findViewById(android.support.v7.appcompat.R.id.search_src_text); + if (view instanceof AutoCompleteTextView) { + ((AutoCompleteTextView) view).setThreshold(1); + } + + MenuItemCompat.setOnActionExpandListener(mSearchMenuItem, new MenuItemCompat.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + if (getPostListType() != ReaderPostListType.SEARCH_RESULTS) { + AnalyticsTracker.track(AnalyticsTracker.Stat.READER_SEARCH_LOADED); + } + resetPostAdapter(ReaderPostListType.SEARCH_RESULTS); + showSearchMessage(); + mSettingsMenuItem.setVisible(false); + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + hideSearchMessage(); + resetSearchSuggestionAdapter(); + mSettingsMenuItem.setVisible(true); + mCurrentSearchQuery = null; + + // return to the followed tag that was showing prior to searching + resetPostAdapter(ReaderPostListType.TAG_FOLLOWED); + + return true; + } + }); + + mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + submitSearchQuery(query); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + if (TextUtils.isEmpty(newText)) { + showSearchMessage(); + } else { + populateSearchSuggestionAdapter(newText); + } + return true; + } + } + ); + } + + /* + * start the search service to search for posts matching the current query - the passed + * offset is used during infinite scroll, pass zero for initial search + */ + private void updatePostsInCurrentSearch(int offset) { + ReaderSearchService.startService(getActivity(), mCurrentSearchQuery, offset); + } + + private void submitSearchQuery(@NonNull String query) { + if (!isAdded()) return; + + mSearchView.clearFocus(); // this will hide suggestions and the virtual keyboard + hideSearchMessage(); + + // remember this query for future suggestions + String trimQuery = query != null ? query.trim() : ""; + ReaderSearchTable.addOrUpdateQueryString(trimQuery); + + // remove cached results for this search - search results are ephemeral so each search + // should be treated as a "fresh" one + ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(trimQuery); + ReaderPostTable.deletePostsWithTag(searchTag); + + mPostAdapter.setCurrentTag(searchTag); + mCurrentSearchQuery = trimQuery; + updatePostsInCurrentSearch(0); + + // track that the user performed a search + if (!trimQuery.equals("")) { + Map<String, Object> properties = new HashMap<>(); + properties.put("query", trimQuery); + AnalyticsTracker.track(AnalyticsTracker.Stat.READER_SEARCH_PERFORMED, properties); + } + } + + /* + * reuse "empty" view to let user know what they're querying + */ + private void showSearchMessage() { + if (!isAdded()) return; + + // clear posts so only the empty view is visible + getPostAdapter().clear(); + + setEmptyTitleAndDescription(false); + showEmptyView(); + } + + private void hideSearchMessage() { + hideEmptyView(); + } + + /* + * create and assign the suggestion adapter for the search view + */ + private void createSearchSuggestionAdapter() { + mSearchSuggestionAdapter = new ReaderSearchSuggestionAdapter(getActivity()); + mSearchView.setSuggestionsAdapter(mSearchSuggestionAdapter); + + mSearchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() { + @Override + public boolean onSuggestionSelect(int position) { + return false; + } + + @Override + public boolean onSuggestionClick(int position) { + String query = mSearchSuggestionAdapter.getSuggestion(position); + if (!TextUtils.isEmpty(query)) { + mSearchView.setQuery(query, true); + } + return true; + } + }); + } + + private void populateSearchSuggestionAdapter(String query) { + if (mSearchSuggestionAdapter == null) { + createSearchSuggestionAdapter(); + } + mSearchSuggestionAdapter.setFilter(query); + } + + private void resetSearchSuggestionAdapter() { + mSearchView.setSuggestionsAdapter(null); + mSearchSuggestionAdapter = null; + } + + /* + * is the search input showing? + */ + private boolean isSearchViewExpanded() { + return mSearchView != null && !mSearchView.isIconified(); + } + + private boolean isSearchViewEmpty() { + return mSearchView != null && mSearchView.getQuery().length() == 0; + } + + @SuppressWarnings("unused") + public void onEventMainThread(ReaderEvents.SearchPostsStarted event) { + if (!isAdded()) return; + + UpdateAction updateAction = event.getOffset() == 0 ? UpdateAction.REQUEST_NEWER : UpdateAction.REQUEST_OLDER; + setIsUpdating(true, updateAction); + setEmptyTitleAndDescription(false); + } + + @SuppressWarnings("unused") + public void onEventMainThread(ReaderEvents.SearchPostsEnded event) { + if (!isAdded()) return; + + UpdateAction updateAction = event.getOffset() == 0 ? UpdateAction.REQUEST_NEWER : UpdateAction.REQUEST_OLDER; + setIsUpdating(false, updateAction); + + // load the results if the search succeeded and it's the current search - note that success + // means the search didn't fail, not necessarily that is has results - which is fine because + // if there aren't results then refreshing will show the empty message + if (event.didSucceed() + && getPostListType() == ReaderPostListType.SEARCH_RESULTS + && event.getQuery().equals(mCurrentSearchQuery)) { + refreshPosts(); + } + } + + /* + * called when user taps follow item in popup menu for a post + */ + private void toggleFollowStatusForPost(final ReaderPost post) { + if (post == null + || !hasPostAdapter() + || !NetworkUtils.checkConnection(getActivity())) { + return; + } + + final boolean isAskingToFollow = !ReaderPostTable.isPostFollowed(post); + + ReaderActions.ActionListener actionListener = new ReaderActions.ActionListener() { + @Override + public void onActionResult(boolean succeeded) { + if (isAdded() && !succeeded) { + int resId = (isAskingToFollow ? R.string.reader_toast_err_follow_blog : R.string.reader_toast_err_unfollow_blog); + ToastUtils.showToast(getActivity(), resId); + getPostAdapter().setFollowStatusForBlog(post.blogId, !isAskingToFollow); + } + } + }; + + if (ReaderBlogActions.followBlogForPost(post, isAskingToFollow, actionListener)) { + getPostAdapter().setFollowStatusForBlog(post.blogId, isAskingToFollow); + } + } + + /* + * blocks the blog associated with the passed post and removes all posts in that blog + * from the adapter + */ + private void blockBlogForPost(final ReaderPost post) { + if (post == null + || !isAdded() + || !hasPostAdapter() + || !NetworkUtils.checkConnection(getActivity())) { + return; + } + + ReaderActions.ActionListener actionListener = new ReaderActions.ActionListener() { + @Override + public void onActionResult(boolean succeeded) { + if (!succeeded && isAdded()) { + ToastUtils.showToast(getActivity(), R.string.reader_toast_err_block_blog, ToastUtils.Duration.LONG); + } + } + }; + + // perform call to block this blog - returns list of posts deleted by blocking so + // they can be restored if the user undoes the block + final BlockedBlogResult blockResult = ReaderBlogActions.blockBlogFromReader(post.blogId, actionListener); + // Only pass the blogID if available. Do not track feedID + AnalyticsUtils.trackWithBlogDetails( + AnalyticsTracker.Stat.READER_BLOG_BLOCKED, + mCurrentBlogId != 0 ? mCurrentBlogId : null + ); + + // remove posts in this blog from the adapter + getPostAdapter().removePostsInBlog(post.blogId); + + // show the undo snackbar enabling the user to undo the block + View.OnClickListener undoListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + ReaderBlogActions.undoBlockBlogFromReader(blockResult); + refreshPosts(); + } + }; + Snackbar.make(getView(), getString(R.string.reader_toast_blog_blocked), Snackbar.LENGTH_LONG) + .setAction(R.string.undo, undoListener) + .show(); + } + + /* + * box/pages animation that appears when loading an empty list + */ + private boolean shouldShowBoxAndPagesAnimation() { + return getPostListType().isTagType(); + } + + private void startBoxAndPagesAnimation() { + if (!isAdded()) return; + + ImageView page1 = (ImageView) mEmptyView.findViewById(R.id.empty_tags_box_page1); + ImageView page2 = (ImageView) mEmptyView.findViewById(R.id.empty_tags_box_page2); + ImageView page3 = (ImageView) mEmptyView.findViewById(R.id.empty_tags_box_page3); + + page1.startAnimation(AnimationUtils.loadAnimation(getActivity(), R.anim.box_with_pages_slide_up_page1)); + page2.startAnimation(AnimationUtils.loadAnimation(getActivity(), R.anim.box_with_pages_slide_up_page2)); + page3.startAnimation(AnimationUtils.loadAnimation(getActivity(), R.anim.box_with_pages_slide_up_page3)); + } + + private void setEmptyTitleAndDescription(boolean requestFailed) { + if (!isAdded()) return; + + String title; + String description = null; + + if (!NetworkUtils.isNetworkAvailable(getActivity())) { + title = getString(R.string.reader_empty_posts_no_connection); + } else if (requestFailed) { + title = getString(R.string.reader_empty_posts_request_failed); + } else if (isUpdating() && getPostListType() != ReaderPostListType.SEARCH_RESULTS) { + title = getString(R.string.reader_empty_posts_in_tag_updating); + } else { + switch (getPostListType()) { + case TAG_FOLLOWED: + if (getCurrentTag().isFollowedSites()) { + if (ReaderBlogTable.hasFollowedBlogs()) { + title = getString(R.string.reader_empty_followed_blogs_no_recent_posts_title); + description = getString(R.string.reader_empty_followed_blogs_no_recent_posts_description); + } else { + title = getString(R.string.reader_empty_followed_blogs_title); + description = getString(R.string.reader_empty_followed_blogs_description); + } + } else if (getCurrentTag().isPostsILike()) { + title = getString(R.string.reader_empty_posts_liked); + } else if (getCurrentTag().isListTopic()) { + title = getString(R.string.reader_empty_posts_in_custom_list); + } else { + title = getString(R.string.reader_empty_posts_in_tag); + } + break; + + case BLOG_PREVIEW: + title = getString(R.string.reader_empty_posts_in_blog); + break; + + case SEARCH_RESULTS: + if (isSearchViewEmpty() || TextUtils.isEmpty(mCurrentSearchQuery)) { + title = getString(R.string.reader_label_post_search_explainer); + } else if (isUpdating()) { + title = getString(R.string.reader_label_post_search_running); + } else { + title = getString(R.string.reader_empty_posts_in_search_title); + String formattedQuery = "<em>" + mCurrentSearchQuery + "</em>"; + description = String.format(getString(R.string.reader_empty_posts_in_search_description), formattedQuery); + } + break; + + default: + title = getString(R.string.reader_empty_posts_in_tag); + break; + } + } + + setEmptyTitleAndDescription(title, description); + } + + private void setEmptyTitleAndDescription(@NonNull String title, String description) { + if (!isAdded()) return; + + TextView titleView = (TextView) mEmptyView.findViewById(R.id.title_empty); + titleView.setText(title); + + TextView descriptionView = (TextView) mEmptyView.findViewById(R.id.description_empty); + if (description == null) { + descriptionView.setVisibility(View.INVISIBLE); + } else { + if (description.contains("<") && description.contains(">")) { + descriptionView.setText(Html.fromHtml(description)); + } else { + descriptionView.setText(description); + } + descriptionView.setVisibility(View.VISIBLE); + } + + mEmptyViewBoxImages.setVisibility(shouldShowBoxAndPagesAnimation() ? View.VISIBLE : View.GONE); + } + + private void showEmptyView() { + if (isAdded()) { + mEmptyView.setVisibility(View.VISIBLE); + } + } + + private void hideEmptyView() { + if (isAdded()) { + mEmptyView.setVisibility(View.GONE); + } + } + + /* + * called by post adapter when data has been loaded + */ + private final ReaderInterfaces.DataLoadedListener mDataLoadedListener = new ReaderInterfaces.DataLoadedListener() { + @Override + public void onDataLoaded(boolean isEmpty) { + if (!isAdded()) { + return; + } + mRecyclerView.setRefreshing(false); + if (isEmpty) { + setEmptyTitleAndDescription(false); + showEmptyView(); + if (shouldShowBoxAndPagesAnimation()) { + startBoxAndPagesAnimation(); + } + } else { + hideEmptyView(); + if (mRestorePosition > 0) { + AppLog.d(T.READER, "reader post list > restoring position"); + mRecyclerView.scrollRecycleViewToPosition(mRestorePosition); + } + } + mRestorePosition = 0; + } + }; + + /* + * called by post adapter to load older posts when user scrolls to the last post + */ + private final ReaderActions.DataRequestedListener mDataRequestedListener = new ReaderActions.DataRequestedListener() { + @Override + public void onRequestData() { + // skip if update is already in progress + if (isUpdating()) { + return; + } + + // request older posts unless we already have the max # to show + switch (getPostListType()) { + case TAG_FOLLOWED: + case TAG_PREVIEW: + if (ReaderPostTable.getNumPostsWithTag(mCurrentTag) < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY) { + // request older posts + updatePostsWithTag(getCurrentTag(), UpdateAction.REQUEST_OLDER); + AnalyticsTracker.track(AnalyticsTracker.Stat.READER_INFINITE_SCROLL); + } + break; + + case BLOG_PREVIEW: + int numPosts; + if (mCurrentFeedId != 0) { + numPosts = ReaderPostTable.getNumPostsInFeed(mCurrentFeedId); + } else { + numPosts = ReaderPostTable.getNumPostsInBlog(mCurrentBlogId); + } + if (numPosts < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY) { + updatePostsInCurrentBlogOrFeed(UpdateAction.REQUEST_OLDER); + AnalyticsTracker.track(AnalyticsTracker.Stat.READER_INFINITE_SCROLL); + } + break; + + case SEARCH_RESULTS: + ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(mCurrentSearchQuery); + int offset = ReaderPostTable.getNumPostsWithTag(searchTag); + if (offset < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY) { + updatePostsInCurrentSearch(offset); + AnalyticsTracker.track(AnalyticsTracker.Stat.READER_INFINITE_SCROLL); + } + break; + } + } + }; + + private ReaderPostAdapter getPostAdapter() { + if (mPostAdapter == null) { + AppLog.d(T.READER, "reader post list > creating post adapter"); + Context context = WPActivityUtils.getThemedContext(getActivity()); + mPostAdapter = new ReaderPostAdapter(context, getPostListType()); + mPostAdapter.setOnPostSelectedListener(this); + mPostAdapter.setOnTagSelectedListener(this); + mPostAdapter.setOnPostPopupListener(this); + mPostAdapter.setOnDataLoadedListener(mDataLoadedListener); + mPostAdapter.setOnDataRequestedListener(mDataRequestedListener); + if (getActivity() instanceof ReaderSiteHeaderView.OnBlogInfoLoadedListener) { + mPostAdapter.setOnBlogInfoLoadedListener((ReaderSiteHeaderView.OnBlogInfoLoadedListener) getActivity()); + } + if (getPostListType().isTagType()) { + mPostAdapter.setCurrentTag(getCurrentTag()); + } else if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) { + mPostAdapter.setCurrentBlogAndFeed(mCurrentBlogId, mCurrentFeedId); + } else if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) { + ReaderTag searchTag = ReaderSearchService.getTagForSearchQuery(mCurrentSearchQuery); + mPostAdapter.setCurrentTag(searchTag); + } + } + return mPostAdapter; + } + + private boolean hasPostAdapter() { + return (mPostAdapter != null); + } + + private boolean isPostAdapterEmpty() { + return (mPostAdapter == null || mPostAdapter.isEmpty()); + } + + private boolean isCurrentTag(final ReaderTag tag) { + return ReaderTag.isSameTag(tag, mCurrentTag); + } + private boolean isCurrentTagName(String tagName) { + return (tagName != null && tagName.equalsIgnoreCase(getCurrentTagName())); + } + + private ReaderTag getCurrentTag() { + return mCurrentTag; + } + + private String getCurrentTagName() { + return (mCurrentTag != null ? mCurrentTag.getTagSlug() : ""); + } + + private boolean hasCurrentTag() { + return mCurrentTag != null; + } + + private void setCurrentTag(final ReaderTag tag) { + if (tag == null) { + return; + } + + // skip if this is already the current tag and the post adapter is already showing it + if (isCurrentTag(tag) + && hasPostAdapter() + && getPostAdapter().isCurrentTag(tag)) { + return; + } + + mCurrentTag = tag; + + switch (getPostListType()) { + case TAG_FOLLOWED: + // remember this as the current tag if viewing followed tag + AppPrefs.setReaderTag(tag); + break; + case TAG_PREVIEW: + mTagPreviewHistory.push(tag.getTagSlug()); + break; + } + + getPostAdapter().setCurrentTag(tag); + hideNewPostsBar(); + showLoadingProgress(false); + + updateCurrentTagIfTime(); + } + + /* + * called by the activity when user hits the back button - returns true if the back button + * is handled here and should be ignored by the activity + */ + @Override + public boolean onActivityBackPressed() { + if (isSearchViewExpanded()) { + mSearchMenuItem.collapseActionView(); + return true; + } else if (goBackInTagHistory()) { + return true; + } else { + return false; + } + } + + /* + * when previewing posts with a specific tag, a history of previewed tags is retained so + * the user can navigate back through them - this is faster and requires less memory + * than creating a new fragment for each previewed tag + */ + private boolean goBackInTagHistory() { + if (mTagPreviewHistory.empty()) { + return false; + } + + String tagName = mTagPreviewHistory.pop(); + if (isCurrentTagName(tagName)) { + if (mTagPreviewHistory.empty()) { + return false; + } + tagName = mTagPreviewHistory.pop(); + } + + ReaderTag newTag = ReaderUtils.getTagFromTagName(tagName, ReaderTagType.FOLLOWED); + setCurrentTag(newTag); + + return true; + } + + /* + * load tags on which the main data will be filtered + */ + private void loadTags(FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener) { + new LoadTagsTask(listener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + /* + * refresh adapter so latest posts appear + */ + private void refreshPosts() { + hideNewPostsBar(); + if (hasPostAdapter()) { + getPostAdapter().refresh(); + } + } + + /* + * same as above but clears posts before refreshing + */ + private void reloadPosts() { + hideNewPostsBar(); + if (hasPostAdapter()) { + getPostAdapter().reload(); + } + } + + /* + * reload the list of tags for the dropdown filter + */ + private void reloadTags() { + if (isAdded() && mRecyclerView != null) { + mRecyclerView.refreshFilterCriteriaOptions(); + } + } + + /* + * get posts for the current blog from the server + */ + private void updatePostsInCurrentBlogOrFeed(final UpdateAction updateAction) { + if (!NetworkUtils.isNetworkAvailable(getActivity())) { + AppLog.i(T.READER, "reader post list > network unavailable, canceled blog update"); + return; + } + if (mCurrentFeedId != 0) { + ReaderPostService.startServiceForFeed(getActivity(), mCurrentFeedId, updateAction); + } else { + ReaderPostService.startServiceForBlog(getActivity(), mCurrentBlogId, updateAction); + } + } + + @SuppressWarnings("unused") + public void onEventMainThread(ReaderEvents.UpdatePostsStarted event) { + if (!isAdded()) return; + + setIsUpdating(true, event.getAction()); + setEmptyTitleAndDescription(false); + } + + @SuppressWarnings("unused") + public void onEventMainThread(ReaderEvents.UpdatePostsEnded event) { + if (!isAdded()) return; + + setIsUpdating(false, event.getAction()); + if (event.getReaderTag() != null && !isCurrentTag(event.getReaderTag())) { + return; + } + + // don't show new posts if user is searching - posts will automatically + // appear when search is exited + if (isSearchViewExpanded() + || getPostListType() == ReaderPostListType.SEARCH_RESULTS) { + return; + } + + // determine whether to show the "new posts" bar - when this is shown, the newly + // downloaded posts aren't displayed until the user taps the bar - only appears + // when there are new posts in a followed tag and the user has scrolled the list + // beyond the first post + if (event.getResult() == ReaderActions.UpdateResult.HAS_NEW + && event.getAction() == UpdateAction.REQUEST_NEWER + && getPostListType() == ReaderPostListType.TAG_FOLLOWED + && !isPostAdapterEmpty() + && (!isAdded() || !mRecyclerView.isFirstItemVisible())) { + showNewPostsBar(); + } else if (event.getResult().isNewOrChanged()) { + refreshPosts(); + } else { + boolean requestFailed = (event.getResult() == ReaderActions.UpdateResult.FAILED); + setEmptyTitleAndDescription(requestFailed); + // if we requested posts in order to fill a gap but the request failed or didn't + // return any posts, reload the adapter so the gap marker is reset (hiding its + // progress bar) + if (event.getAction() == UpdateAction.REQUEST_OLDER_THAN_GAP) { + reloadPosts(); + } + } + } + + /* + * get latest posts for this tag from the server + */ + private void updatePostsWithTag(ReaderTag tag, UpdateAction updateAction) { + if (!isAdded()) return; + + if (!NetworkUtils.isNetworkAvailable(getActivity())) { + AppLog.i(T.READER, "reader post list > network unavailable, canceled tag update"); + return; + } + if (tag == null) { + AppLog.w(T.READER, "null tag passed to updatePostsWithTag"); + return; + } + AppLog.d(T.READER, "reader post list > updating tag " + tag.getTagNameForLog() + ", updateAction=" + updateAction.name()); + ReaderPostService.startServiceForTag(getActivity(), tag, updateAction); + } + + private void updateCurrentTag() { + updatePostsWithTag(getCurrentTag(), UpdateAction.REQUEST_NEWER); + } + + /* + * update the current tag if it's time to do so - note that the check is done in the + * background since it can be expensive and this is called when the fragment is + * resumed, which on slower devices can result in a janky experience + */ + private void updateCurrentTagIfTime() { + if (!isAdded() || !hasCurrentTag()) { + return; + } + new Thread() { + @Override + public void run() { + if (ReaderTagTable.shouldAutoUpdateTag(getCurrentTag()) && isAdded()) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + updateCurrentTag(); + } + }); + } + } + }.start(); + } + + private boolean isUpdating() { + return mIsUpdating; + } + + /* + * show/hide progress bar which appears at the bottom of the activity when loading more posts + */ + private void showLoadingProgress(boolean showProgress) { + if (isAdded() && mProgress != null) { + if (showProgress) { + mProgress.bringToFront(); + mProgress.setVisibility(View.VISIBLE); + } else { + mProgress.setVisibility(View.GONE); + } + } + } + + private void setIsUpdating(boolean isUpdating, UpdateAction updateAction) { + if (!isAdded() || mIsUpdating == isUpdating) { + return; + } + + if (updateAction == UpdateAction.REQUEST_OLDER) { + // show/hide progress bar at bottom if these are older posts + showLoadingProgress(isUpdating); + } else if (isUpdating && isPostAdapterEmpty()) { + // show swipe-to-refresh if update started and no posts are showing + mRecyclerView.setRefreshing(true); + } else if (!isUpdating) { + // hide swipe-to-refresh progress if update is complete + mRecyclerView.setRefreshing(false); + } + mIsUpdating = isUpdating; + + // if swipe-to-refresh isn't active, keep it disabled during an update - this prevents + // doing a refresh while another update is already in progress + if (mRecyclerView != null && !mRecyclerView.isRefreshing()) { + mRecyclerView.setSwipeToRefreshEnabled(!isUpdating && isSwipeToRefreshSupported()); + } + } + + /* + * swipe-to-refresh isn't supported for search results since they're really brief snapshots + * and are unlikely to show new posts due to the way they're sorted + */ + private boolean isSwipeToRefreshSupported() { + return getPostListType() != ReaderPostListType.SEARCH_RESULTS; + } + + /* + * bar that appears at the top when new posts have been retrieved + */ + private boolean isNewPostsBarShowing() { + return (mNewPostsBar != null && mNewPostsBar.getVisibility() == View.VISIBLE); + } + + /* + * scroll listener assigned to the recycler when the "new posts" bar 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); + hideNewPostsBar(); + } + }; + + private void showNewPostsBar() { + if (!isAdded() || isNewPostsBarShowing()) { + return; + } + + AniUtils.startAnimation(mNewPostsBar, R.anim.reader_top_bar_in); + mNewPostsBar.setVisibility(View.VISIBLE); + + // 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() && isNewPostsBarShowing()) { + mRecyclerView.addOnScrollListener(mOnScrollListener); + } + } + }, 1000L); + + // remove the gap marker if it's showing, since it's no longer valid + getPostAdapter().removeGapMarker(); + } + + private void hideNewPostsBar() { + if (!isAdded() || !isNewPostsBarShowing() || mIsAnimatingOutNewPostsBar) { + return; + } + + mIsAnimatingOutNewPostsBar = true; + + // remove the onScrollListener assigned in showNewPostsBar() + mRecyclerView.removeOnScrollListener(mOnScrollListener); + + Animation.AnimationListener listener = new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { } + @Override + public void onAnimationEnd(Animation animation) { + if (isAdded()) { + mNewPostsBar.setVisibility(View.GONE); + mIsAnimatingOutNewPostsBar = false; + } + } + @Override + public void onAnimationRepeat(Animation animation) { } + }; + AniUtils.startAnimation(mNewPostsBar, R.anim.reader_top_bar_out, listener); + } + + /* + * are we showing all posts with a specific tag (followed or previewed), or all + * posts in a specific blog? + */ + private ReaderPostListType getPostListType() { + return (mPostListType != null ? mPostListType : ReaderTypes.DEFAULT_POST_LIST_TYPE); + } + + /* + * called from adapter when user taps a post + */ + @Override + public void onPostSelected(ReaderPost post) { + if (!isAdded() || post == null) return; + + // "discover" posts that highlight another post should open the original (source) post when tapped + if (post.isDiscoverPost()) { + ReaderPostDiscoverData discoverData = post.getDiscoverData(); + if (discoverData != null && discoverData.getDiscoverType() == ReaderPostDiscoverData.DiscoverType.EDITOR_PICK) { + if (discoverData.getBlogId() != 0 && discoverData.getPostId() != 0) { + ReaderActivityLauncher.showReaderPostDetail( + getActivity(), + discoverData.getBlogId(), + discoverData.getPostId()); + return; + } else if (discoverData.hasPermalink()) { + // if we don't have a blogId/postId, we sadly resort to showing the post + // in a WebView activity - this will happen for non-JP self-hosted + ReaderActivityLauncher.openUrl(getActivity(), discoverData.getPermaLink()); + return; + } + } + } + + // if this is a cross-post, we want to show the original post + if (post.isXpost()) { + ReaderActivityLauncher.showReaderPostDetail(getActivity(), post.xpostBlogId, post.xpostPostId); + return; + } + + ReaderPostListType type = getPostListType(); + + switch (type) { + case TAG_FOLLOWED: + case TAG_PREVIEW: + ReaderActivityLauncher.showReaderPostPagerForTag( + getActivity(), + getCurrentTag(), + getPostListType(), + post.blogId, + post.postId); + break; + case BLOG_PREVIEW: + ReaderActivityLauncher.showReaderPostPagerForBlog( + getActivity(), + post.blogId, + post.postId); + break; + case SEARCH_RESULTS: + AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_SEARCH_RESULT_TAPPED, post); + ReaderActivityLauncher.showReaderPostDetail(getActivity(), post.blogId, post.postId); + break; + } + } + + /* + * called from adapter when user taps a tag on a post to display tag preview + */ + @Override + public void onTagSelected(String tagName) { + if (!isAdded()) return; + + ReaderTag tag = ReaderUtils.getTagFromTagName(tagName, ReaderTagType.FOLLOWED); + if (getPostListType().equals(ReaderPostListType.TAG_PREVIEW)) { + // user is already previewing a tag, so change current tag in existing preview + setCurrentTag(tag); + } else { + // user isn't previewing a tag, so open in tag preview + ReaderActivityLauncher.showReaderTagPreview(getActivity(), tag); + } + } + + /* + * called when user selects a tag from the tag toolbar + */ + private void onTagChanged(ReaderTag tag) { + if (!isAdded() || isCurrentTag(tag)) return; + + trackTagLoaded(tag); + AppLog.d(T.READER, String.format("reader post list > tag %s displayed", tag.getTagNameForLog())); + setCurrentTag(tag); + } + + private void trackTagLoaded(ReaderTag tag) { + AnalyticsTracker.Stat stat = null; + + if (tag.isDiscover()) { + stat = AnalyticsTracker.Stat.READER_DISCOVER_VIEWED; + } else if (tag.isTagTopic()) { + stat = AnalyticsTracker.Stat.READER_TAG_LOADED; + } else if (tag.isListTopic()) { + stat = AnalyticsTracker.Stat.READER_LIST_LOADED; + } + + if (stat == null) return; + + Map<String, String> properties = new HashMap<>(); + properties.put("tag", tag.getTagSlug()); + + AnalyticsTracker.track(stat, properties); + } + + /* + * called when user taps "..." icon next to a post + */ + @Override + public void onShowPostPopup(View view, final ReaderPost post) { + if (view == null || post == null || !isAdded()) return; + + Context context = view.getContext(); + final ListPopupWindow listPopup = new ListPopupWindow(context); + listPopup.setAnchorView(view); + listPopup.setWidth(context.getResources().getDimensionPixelSize(R.dimen.menu_item_width)); + listPopup.setModal(true); + + List<Integer> menuItems = new ArrayList<>(); + boolean isFollowed = ReaderPostTable.isPostFollowed(post); + if (isFollowed) { + menuItems.add(ReaderMenuAdapter.ITEM_UNFOLLOW); + } else { + menuItems.add(ReaderMenuAdapter.ITEM_FOLLOW); + } + if (getPostListType() == ReaderPostListType.TAG_FOLLOWED) { + menuItems.add(ReaderMenuAdapter.ITEM_BLOCK); + } + listPopup.setAdapter(new ReaderMenuAdapter(context, menuItems)); + listPopup.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + if (!isAdded()) return; + + listPopup.dismiss(); + switch((int) id) { + case ReaderMenuAdapter.ITEM_FOLLOW: + case ReaderMenuAdapter.ITEM_UNFOLLOW: + toggleFollowStatusForPost(post); + break; + case ReaderMenuAdapter.ITEM_BLOCK: + blockBlogForPost(post); + break; + } + } + }); + listPopup.show(); + } + + /* + * purge reader db if it hasn't been done yet + */ + private void purgeDatabaseIfNeeded() { + if (!mHasPurgedReaderDb) { + AppLog.d(T.READER, "reader post list > purging database"); + mHasPurgedReaderDb = true; + ReaderDatabase.purgeAsync(); + } + } + + /* + * start background service to get the latest followed tags and blogs if it's time to do so + */ + private void updateFollowedTagsAndBlogsIfNeeded() { + if (mLastAutoUpdateDt != null) { + int minutesSinceLastUpdate = DateTimeUtils.minutesBetween(mLastAutoUpdateDt, new Date()); + if (minutesSinceLastUpdate < 120) { + return; + } + } + + AppLog.d(T.READER, "reader post list > updating tags and blogs"); + mLastAutoUpdateDt = new Date(); + ReaderUpdateService.startService(getActivity(), EnumSet.of(UpdateTask.TAGS, UpdateTask.FOLLOWED_BLOGS)); + } + + @Override + public void onScrollToTop() { + if (isAdded() && getCurrentPosition() > 0) { + mRecyclerView.smoothScrollToPosition(0); + mRecyclerView.showToolbar(); + } + } + + public static void resetLastUpdateDate() { + mLastAutoUpdateDt = null; + } + + private class LoadTagsTask extends AsyncTask<Void, Void, ReaderTagList> { + + private final FilteredRecyclerView.FilterCriteriaAsyncLoaderListener mFilterCriteriaLoaderListener; + + public LoadTagsTask(FilteredRecyclerView.FilterCriteriaAsyncLoaderListener listener){ + mFilterCriteriaLoaderListener = listener; + } + + @Override + protected ReaderTagList doInBackground(Void... voids) { + ReaderTagList tagList = ReaderTagTable.getDefaultTags(); + tagList.addAll(ReaderTagTable.getCustomListTags()); + tagList.addAll(ReaderTagTable.getFollowedTags()); + return tagList; + } + + @Override + protected void onPostExecute(ReaderTagList tagList) { + if (tagList != null && !tagList.isSameList(mTags)) { + mTags.clear(); + mTags.addAll(tagList); + if (mFilterCriteriaLoaderListener != null) + //noinspection unchecked + mFilterCriteriaLoaderListener.onFilterCriteriasLoaded((List)mTags); + } + } + } + +} + |