diff options
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java')
-rw-r--r-- | WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java | 1171 |
1 files changed, 1171 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java new file mode 100644 index 000000000..da635bfe4 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java @@ -0,0 +1,1171 @@ +package org.wordpress.android.ui.reader; + +import android.app.Activity; +import android.app.Fragment; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +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 android.webkit.WebView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.WordPress; +import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.datasets.ReaderLikeTable; +import org.wordpress.android.datasets.ReaderPostTable; +import org.wordpress.android.models.ReaderPost; +import org.wordpress.android.models.ReaderPostDiscoverData; +import org.wordpress.android.models.ReaderTag; +import org.wordpress.android.models.ReaderTagType; +import org.wordpress.android.ui.main.WPMainActivity; +import org.wordpress.android.ui.reader.ReaderActivityLauncher.OpenUrlType; +import org.wordpress.android.ui.reader.ReaderActivityLauncher.PhotoViewerOption; +import org.wordpress.android.ui.reader.ReaderInterfaces.AutoHideToolbarListener; +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.ReaderPostActions; +import org.wordpress.android.ui.reader.models.ReaderBlogIdPostId; +import org.wordpress.android.ui.reader.models.ReaderRelatedPost; +import org.wordpress.android.ui.reader.models.ReaderRelatedPostList; +import org.wordpress.android.ui.reader.utils.ReaderUtils; +import org.wordpress.android.ui.reader.utils.ReaderVideoUtils; +import org.wordpress.android.ui.reader.views.ReaderFollowButton; +import org.wordpress.android.ui.reader.views.ReaderIconCountView; +import org.wordpress.android.ui.reader.views.ReaderLikingUsersView; +import org.wordpress.android.ui.reader.views.ReaderWebView; +import org.wordpress.android.ui.reader.views.ReaderWebView.ReaderCustomViewListener; +import org.wordpress.android.ui.reader.views.ReaderWebView.ReaderWebViewPageFinishedListener; +import org.wordpress.android.ui.reader.views.ReaderWebView.ReaderWebViewUrlClickListener; +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.GravatarUtils; +import org.wordpress.android.util.NetworkUtils; +import org.wordpress.android.util.PhotonUtils; +import org.wordpress.android.util.ToastUtils; +import org.wordpress.android.util.UrlUtils; +import org.wordpress.android.util.helpers.SwipeToRefreshHelper; +import org.wordpress.android.util.widgets.CustomSwipeRefreshLayout; +import org.wordpress.android.widgets.WPNetworkImageView; +import org.wordpress.android.widgets.WPScrollView; +import org.wordpress.android.widgets.WPScrollView.ScrollDirectionListener; + +import java.util.EnumSet; + +import de.greenrobot.event.EventBus; + +public class ReaderPostDetailFragment extends Fragment + implements WPMainActivity.OnActivityBackPressedListener, + ScrollDirectionListener, + ReaderCustomViewListener, + ReaderWebViewPageFinishedListener, + ReaderWebViewUrlClickListener { + + private long mPostId; + private long mBlogId; + private ReaderPost mPost; + private ReaderPostRenderer mRenderer; + private ReaderPostListType mPostListType; + + private final ReaderPostHistory mPostHistory = new ReaderPostHistory(); + + private SwipeToRefreshHelper mSwipeToRefreshHelper; + private WPScrollView mScrollView; + private ViewGroup mLayoutFooter; + private ReaderWebView mReaderWebView; + private ReaderLikingUsersView mLikingUsersView; + private View mLikingUsersDivider; + private View mLikingUsersLabel; + + private boolean mHasAlreadyUpdatedPost; + private boolean mHasAlreadyRequestedPost; + private boolean mIsLoggedOutReader; + private boolean mIsWebViewPaused; + private boolean mIsRelatedPost; + + private int mToolbarHeight; + private String mErrorMessage; + + private boolean mIsToolbarShowing = true; + private AutoHideToolbarListener mAutoHideToolbarListener; + + // min scroll distance before toggling toolbar + private static final float MIN_SCROLL_DISTANCE_Y = 10; + + public static ReaderPostDetailFragment newInstance(long blogId, long postId) { + return newInstance(blogId, postId, false, null); + } + + public static ReaderPostDetailFragment newInstance(long blogId, + long postId, + boolean isRelatedPost, + ReaderPostListType postListType) { + AppLog.d(T.READER, "reader post detail > newInstance"); + + Bundle args = new Bundle(); + args.putLong(ReaderConstants.ARG_BLOG_ID, blogId); + args.putLong(ReaderConstants.ARG_POST_ID, postId); + args.putBoolean(ReaderConstants.ARG_IS_RELATED_POST, isRelatedPost); + if (postListType != null) { + args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, postListType); + } + + ReaderPostDetailFragment fragment = new ReaderPostDetailFragment(); + fragment.setArguments(args); + + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mIsLoggedOutReader = ReaderUtils.isLoggedOutReader(); + if (savedInstanceState != null) { + mPostHistory.restoreInstance(savedInstanceState); + } + } + + @Override + public void setArguments(Bundle args) { + super.setArguments(args); + if (args != null) { + mBlogId = args.getLong(ReaderConstants.ARG_BLOG_ID); + mPostId = args.getLong(ReaderConstants.ARG_POST_ID); + mIsRelatedPost = args.getBoolean(ReaderConstants.ARG_IS_RELATED_POST); + if (args.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) { + mPostListType = (ReaderPostListType) args.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE); + } + } + } + + @SuppressWarnings("deprecation") + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (activity instanceof AutoHideToolbarListener) { + mAutoHideToolbarListener = (AutoHideToolbarListener) activity; + } + mToolbarHeight = activity.getResources().getDimensionPixelSize(R.dimen.toolbar_height); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.reader_fragment_post_detail, container, false); + + CustomSwipeRefreshLayout swipeRefreshLayout = (CustomSwipeRefreshLayout) view.findViewById(R.id.swipe_to_refresh); + + //this fragment hides/shows toolbar with scrolling, which messes up ptr animation position + //so we have to set it manually + int swipeToRefreshOffset = getResources().getDimensionPixelSize(R.dimen.toolbar_content_offset); + swipeRefreshLayout.setProgressViewOffset(false, 0, swipeToRefreshOffset); + + mSwipeToRefreshHelper = new SwipeToRefreshHelper(getActivity(), swipeRefreshLayout, new SwipeToRefreshHelper.RefreshListener() { + @Override + public void onRefreshStarted() { + if (!isAdded()) { + return; + } + + updatePost(); + } + }); + + mScrollView = (WPScrollView) view.findViewById(R.id.scroll_view_reader); + mScrollView.setScrollDirectionListener(this); + + mLayoutFooter = (ViewGroup) view.findViewById(R.id.layout_post_detail_footer); + mLikingUsersView = (ReaderLikingUsersView) view.findViewById(R.id.layout_liking_users_view); + mLikingUsersDivider = view.findViewById(R.id.layout_liking_users_divider); + mLikingUsersLabel = view.findViewById(R.id.text_liking_users_label); + + // setup the ReaderWebView + mReaderWebView = (ReaderWebView) view.findViewById(R.id.reader_webview); + mReaderWebView.setCustomViewListener(this); + mReaderWebView.setUrlClickListener(this); + mReaderWebView.setPageFinishedListener(this); + + // hide footer and scrollView until the post is loaded + mLayoutFooter.setVisibility(View.INVISIBLE); + mScrollView.setVisibility(View.INVISIBLE); + + return view; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mReaderWebView != null) { + mReaderWebView.destroy(); + } + } + + private boolean hasPost() { + return (mPost != null); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + menu.clear(); + inflater.inflate(R.menu.reader_detail, menu); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + + // browse & share require the post to have a URL (some feed-based posts don't have one) + boolean postHasUrl = hasPost() && mPost.hasUrl(); + MenuItem mnuBrowse = menu.findItem(R.id.menu_browse); + if (mnuBrowse != null) { + mnuBrowse.setVisible(postHasUrl); + } + MenuItem mnuShare = menu.findItem(R.id.menu_share); + if (mnuShare != null) { + mnuShare.setVisible(postHasUrl); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int i = item.getItemId(); + if (i == R.id.menu_browse) { + if (hasPost()) { + ReaderActivityLauncher.openUrl(getActivity(), mPost.getUrl(), OpenUrlType.EXTERNAL); + } + return true; + } else if (i == R.id.menu_share) { + AnalyticsTracker.track(AnalyticsTracker.Stat.SHARED_ITEM); + sharePage(); + return true; + } else { + return super.onOptionsItemSelected(item); + } + } + + private ReaderPostListType getPostListType() { + return (mPostListType != null ? mPostListType : ReaderTypes.DEFAULT_POST_LIST_TYPE); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putLong(ReaderConstants.ARG_BLOG_ID, mBlogId); + outState.putLong(ReaderConstants.ARG_POST_ID, mPostId); + + outState.putBoolean(ReaderConstants.ARG_IS_RELATED_POST, mIsRelatedPost); + outState.putBoolean(ReaderConstants.KEY_ALREADY_UPDATED, mHasAlreadyUpdatedPost); + outState.putBoolean(ReaderConstants.KEY_ALREADY_REQUESTED, mHasAlreadyRequestedPost); + + outState.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, getPostListType()); + + mPostHistory.saveInstance(outState); + + if (!TextUtils.isEmpty(mErrorMessage)) { + outState.putString(ReaderConstants.KEY_ERROR_MESSAGE, mErrorMessage); + } + + super.onSaveInstanceState(outState); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setHasOptionsMenu(true); + restoreState(savedInstanceState); + } + + private void restoreState(Bundle savedInstanceState) { + if (savedInstanceState != null) { + mBlogId = savedInstanceState.getLong(ReaderConstants.ARG_BLOG_ID); + mPostId = savedInstanceState.getLong(ReaderConstants.ARG_POST_ID); + mIsRelatedPost = savedInstanceState.getBoolean(ReaderConstants.ARG_IS_RELATED_POST); + mHasAlreadyUpdatedPost = savedInstanceState.getBoolean(ReaderConstants.KEY_ALREADY_UPDATED); + mHasAlreadyRequestedPost = savedInstanceState.getBoolean(ReaderConstants.KEY_ALREADY_REQUESTED); + if (savedInstanceState.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) { + mPostListType = (ReaderPostListType) savedInstanceState.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE); + } + if (savedInstanceState.containsKey(ReaderConstants.KEY_ERROR_MESSAGE)) { + mErrorMessage = savedInstanceState.getString(ReaderConstants.KEY_ERROR_MESSAGE); + } + } + } + + @Override + public void onStart() { + super.onStart(); + EventBus.getDefault().register(this); + if (!hasPost()) { + showPost(); + } + } + + @Override + public void onStop() { + super.onStop(); + EventBus.getDefault().unregister(this); + } + + /* + * 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() { + return goBackInPostHistory(); + } + + /* + * changes the like on the passed post + */ + private void togglePostLike() { + if (!isAdded() || !hasPost() || !NetworkUtils.checkConnection(getActivity())) { + return; + } + + boolean isAskingToLike = !mPost.isLikedByCurrentUser; + ReaderIconCountView likeCount = (ReaderIconCountView) getView().findViewById(R.id.count_likes); + likeCount.setSelected(isAskingToLike); + ReaderAnim.animateLikeButton(likeCount.getImageView(), isAskingToLike); + + boolean success = ReaderPostActions.performLikeAction(mPost, isAskingToLike); + if (!success) { + likeCount.setSelected(!isAskingToLike); + return; + } + + // get the post again since it has changed, then refresh to show changes + mPost = ReaderPostTable.getPost(mBlogId, mPostId, false); + refreshLikes(); + refreshIconCounts(); + + if (isAskingToLike) { + AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_LIKED, mPost); + } else { + AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_UNLIKED, mPost); + } + } + + /* + * user tapped follow button to follow/unfollow the blog this post is from + */ + private void togglePostFollowed() { + if (!isAdded() || !hasPost()) { + return; + } + + final boolean isAskingToFollow = !ReaderPostTable.isPostFollowed(mPost); + final ReaderFollowButton followButton = (ReaderFollowButton) getView().findViewById(R.id.follow_button); + + ReaderActions.ActionListener listener = new ReaderActions.ActionListener() { + @Override + public void onActionResult(boolean succeeded) { + if (!isAdded()) { + return; + } + followButton.setEnabled(true); + if (!succeeded) { + int resId = (isAskingToFollow ? R.string.reader_toast_err_follow_blog : R.string.reader_toast_err_unfollow_blog); + ToastUtils.showToast(getActivity(), resId); + followButton.setIsFollowedAnimated(!isAskingToFollow); + } + } + }; + + followButton.setEnabled(false); + + if (ReaderBlogActions.followBlogForPost(mPost, isAskingToFollow, listener)) { + followButton.setIsFollowedAnimated(isAskingToFollow); + } + } + + /* + * display the standard Android share chooser to share this post + */ + private static final int MAX_SHARE_TITLE_LEN = 100; + + private void sharePage() { + if (!isAdded() || !hasPost()) { + return; + } + + final String url = (mPost.hasShortUrl() ? mPost.getShortUrl() : mPost.getUrl()); + final String shareText; + + if (mPost.hasTitle()) { + final String title; + // we don't know where the user will choose to share, so enforce a max title length + // in order to fit a tweet with some extra room for the URL and user edits + if (mPost.getTitle().length() > MAX_SHARE_TITLE_LEN) { + title = mPost.getTitle().substring(0, MAX_SHARE_TITLE_LEN).trim() + "…"; + } else { + title = mPost.getTitle().trim(); + } + shareText = title + " - " + url; + } else { + shareText = url; + } + + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TEXT, shareText); + intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.reader_share_subject, getString(R.string.app_name))); + try { + startActivity(Intent.createChooser(intent, getString(R.string.reader_share_link))); + } catch (android.content.ActivityNotFoundException ex) { + ToastUtils.showToast(getActivity(), R.string.reader_toast_err_share_intent); + } + } + + /* + * replace the current post with the passed one + */ + private void replacePost(long blogId, long postId) { + mBlogId = blogId; + mPostId = postId; + mHasAlreadyRequestedPost = false; + mHasAlreadyUpdatedPost = false; + + // hide views that would show info for the previous post - these will be re-displayed + // with the correct info once the new post loads + getView().findViewById(R.id.container_related_posts).setVisibility(View.GONE); + getView().findViewById(R.id.text_related_posts_label).setVisibility(View.GONE); + + mLikingUsersView.setVisibility(View.GONE); + mLikingUsersDivider.setVisibility(View.GONE); + mLikingUsersLabel.setVisibility(View.GONE); + + // clear the webView - otherwise it will remain scrolled to where the user scrolled to + mReaderWebView.clearContent(); + + // make sure the toolbar and footer are showing + showToolbar(true); + showFooter(true); + + // now show the passed post + showPost(); + } + + /* + * request posts related to the current one - only available for wp.com + */ + private void requestRelatedPosts() { + if (hasPost() && mPost.isWP()) { + ReaderPostActions.requestRelatedPosts(mPost); + } + } + + /* + * related posts were retrieved, so show them + */ + @SuppressWarnings("unused") + public void onEventMainThread(ReaderEvents.RelatedPostsUpdated event) { + if (!isAdded() || !hasPost()) return; + + // make sure this is for the current post + if (event.getSourcePost().postId == mPostId && event.getSourcePost().blogId == mBlogId) { + showRelatedPosts(event.getRelatedPosts()); + } + } + + private void showRelatedPosts(@NonNull ReaderRelatedPostList relatedPosts) { + // locate the related posts container and remove any existing related post views + ViewGroup container = (ViewGroup) getView().findViewById(R.id.container_related_posts); + container.removeAllViews(); + + // add a separate view for each related post + LayoutInflater inflater = LayoutInflater.from(getActivity()); + int imageSize = DisplayUtils.dpToPx(getActivity(), getResources().getDimensionPixelSize(R.dimen.reader_related_post_image_size)); + for (int index = 0; index < relatedPosts.size(); index++) { + final ReaderRelatedPost relatedPost = relatedPosts.get(index); + + View postView = inflater.inflate(R.layout.reader_related_post, container, false); + TextView txtTitle = (TextView) postView.findViewById(R.id.text_related_post_title); + TextView txtByline = (TextView) postView.findViewById(R.id.text_related_post_byline); + WPNetworkImageView imgFeatured = (WPNetworkImageView) postView.findViewById(R.id.image_related_post); + + txtTitle.setText(relatedPost.getTitle()); + txtByline.setText(relatedPost.getByline()); + + imgFeatured.setVisibility(relatedPost.hasFeaturedImage() ? View.VISIBLE : View.GONE); + if (relatedPost.hasFeaturedImage()) { + String imageUrl = PhotonUtils.getPhotonImageUrl(relatedPost.getFeaturedImage(), imageSize, imageSize); + imgFeatured.setImageUrl(imageUrl, WPNetworkImageView.ImageType.PHOTO_ROUNDED); + imgFeatured.setVisibility(View.VISIBLE); + } + + // tapping this view should open the related post detail + postView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + showRelatedPostDetail(relatedPost.getBlogId(), relatedPost.getPostId()); + } + }); + + container.addView(postView); + + // add a divider below all but the last related post + if (index < relatedPosts.size() - 1) { + View dividerView = inflater.inflate(R.layout.reader_related_post_divider, container, false); + container.addView(dividerView); + } + } + + View label = getView().findViewById(R.id.text_related_posts_label); + if (label.getVisibility() != View.VISIBLE) { + AniUtils.fadeIn(label, AniUtils.Duration.MEDIUM); + } + if (container.getVisibility() != View.VISIBLE) { + AniUtils.fadeIn(container, AniUtils.Duration.MEDIUM); + } + } + + /* + * user clicked a single related post - if we're already viewing a related post, add it to the + * history stack so the user can back-button through the history - otherwise start a new detail + * activity for this related post + */ + private void showRelatedPostDetail(long blogId, long postId) { + AnalyticsUtils.trackWithReaderPostDetails( + AnalyticsTracker.Stat.READER_RELATED_POST_CLICKED, blogId, postId); + + if (mIsRelatedPost) { + mPostHistory.push(new ReaderBlogIdPostId(mPost.blogId, mPost.postId)); + replacePost(blogId, postId); + } else { + ReaderActivityLauncher.showReaderPostDetail(getActivity(), blogId, postId, true); + } + } + + /* + * if the fragment is maintaining a backstack of posts, navigate to the previous one + */ + protected boolean goBackInPostHistory() { + if (!mPostHistory.isEmpty()) { + ReaderBlogIdPostId ids = mPostHistory.pop(); + replacePost(ids.getBlogId(), ids.getPostId()); + return true; + } else { + return false; + } + } + + /* + * get the latest version of this post + */ + private void updatePost() { + if (!hasPost() || !mPost.isWP()) { + setRefreshing(false); + return; + } + + final int numLikesBefore = ReaderLikeTable.getNumLikesForPost(mPost); + + ReaderActions.UpdateResultListener resultListener = new ReaderActions.UpdateResultListener() { + @Override + public void onUpdateResult(ReaderActions.UpdateResult result) { + if (!isAdded()) { + return; + } + // if the post has changed, reload it from the db and update the like/comment counts + if (result.isNewOrChanged()) { + mPost = ReaderPostTable.getPost(mBlogId, mPostId, false); + refreshIconCounts(); + } + // refresh likes if necessary - done regardless of whether the post has changed + // since it's possible likes weren't stored until the post was updated + if (result != ReaderActions.UpdateResult.FAILED + && numLikesBefore != ReaderLikeTable.getNumLikesForPost(mPost)) { + refreshLikes(); + } + + setRefreshing(false); + } + }; + ReaderPostActions.updatePost(mPost, resultListener); + } + + private void refreshIconCounts() { + if (!isAdded() || !hasPost() || !canShowFooter()) { + return; + } + + final ReaderIconCountView countLikes = (ReaderIconCountView) getView().findViewById(R.id.count_likes); + final ReaderIconCountView countComments = (ReaderIconCountView) getView().findViewById(R.id.count_comments); + + if (canShowCommentCount()) { + countComments.setCount(mPost.numReplies); + countComments.setVisibility(View.VISIBLE); + countComments.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ReaderActivityLauncher.showReaderComments(getActivity(), mPost.blogId, mPost.postId); + } + }); + } else { + countComments.setVisibility(View.INVISIBLE); + countComments.setOnClickListener(null); + } + + if (canShowLikeCount()) { + countLikes.setCount(mPost.numLikes); + countLikes.setVisibility(View.VISIBLE); + countLikes.setSelected(mPost.isLikedByCurrentUser); + if (mIsLoggedOutReader) { + countLikes.setEnabled(false); + } else if (mPost.canLikePost()) { + countLikes.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + togglePostLike(); + } + }); + } + // if we know refreshLikes() is going to show the liking users, force liking user + // views to take up space right now + if (mPost.numLikes > 0 && mLikingUsersView.getVisibility() == View.GONE) { + mLikingUsersView.setVisibility(View.INVISIBLE); + mLikingUsersDivider.setVisibility(View.INVISIBLE); + mLikingUsersLabel.setVisibility(View.INVISIBLE); + } + } else { + countLikes.setVisibility(View.INVISIBLE); + countLikes.setOnClickListener(null); + } + } + + /* + * show latest likes for this post + */ + private void refreshLikes() { + if (!isAdded() || !hasPost() || !mPost.canLikePost()) { + return; + } + + // nothing more to do if no likes + if (mPost.numLikes == 0) { + mLikingUsersView.setVisibility(View.GONE); + mLikingUsersDivider.setVisibility(View.GONE); + mLikingUsersLabel.setVisibility(View.GONE); + return; + } + + // clicking likes view shows activity displaying all liking users + mLikingUsersView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + ReaderActivityLauncher.showReaderLikingUsers(getActivity(), mPost.blogId, mPost.postId); + } + }); + + mLikingUsersDivider.setVisibility(View.VISIBLE); + mLikingUsersLabel.setVisibility(View.VISIBLE); + mLikingUsersView.setVisibility(View.VISIBLE); + mLikingUsersView.showLikingUsers(mPost); + } + + private boolean showPhotoViewer(String imageUrl, View sourceView, int startX, int startY) { + if (!isAdded() || TextUtils.isEmpty(imageUrl)) { + return false; + } + + // make sure this is a valid web image (could be file: or data:) + if (!imageUrl.startsWith("http")) { + return false; + } + + String postContent = (mRenderer != null ? mRenderer.getRenderedHtml() : null); + boolean isPrivatePost = (mPost != null && mPost.isPrivate); + EnumSet<PhotoViewerOption> options = EnumSet.noneOf(PhotoViewerOption.class); + if (isPrivatePost) { + options.add(ReaderActivityLauncher.PhotoViewerOption.IS_PRIVATE_IMAGE); + } + + ReaderActivityLauncher.showReaderPhotoViewer( + getActivity(), + imageUrl, + postContent, + sourceView, + options, + startX, + startY); + + return true; + } + + /* + * called when the post doesn't exist in local db, need to get it from server + */ + private void requestPost() { + final ProgressBar progress = (ProgressBar) getView().findViewById(R.id.progress_loading); + progress.setVisibility(View.VISIBLE); + progress.bringToFront(); + + ReaderActions.OnRequestListener listener = new ReaderActions.OnRequestListener() { + @Override + public void onSuccess() { + if (isAdded()) { + progress.setVisibility(View.GONE); + showPost(); + } + } + + @Override + public void onFailure(int statusCode) { + if (isAdded()) { + progress.setVisibility(View.GONE); + int errMsgResId; + if (!NetworkUtils.isNetworkAvailable(getActivity())) { + errMsgResId = R.string.no_network_message; + } else { + switch (statusCode) { + case 401: + case 403: + errMsgResId = R.string.reader_err_get_post_not_authorized; + break; + case 404: + errMsgResId = R.string.reader_err_get_post_not_found; + break; + default: + errMsgResId = R.string.reader_err_get_post_generic; + break; + } + } + showError(getString(errMsgResId)); + } + } + }; + ReaderPostActions.requestPost(mBlogId, mPostId, listener); + } + + /* + * shows an error message in the middle of the screen - used when requesting post fails + */ + private void showError(String errorMessage) { + if (!isAdded()) return; + + TextView txtError = (TextView) getView().findViewById(R.id.text_error); + txtError.setText(errorMessage); + if (txtError.getVisibility() != View.VISIBLE) { + AniUtils.fadeIn(txtError, AniUtils.Duration.MEDIUM); + } + mErrorMessage = errorMessage; + } + + private void showPost() { + if (mIsPostTaskRunning) { + AppLog.w(T.READER, "reader post detail > show post task already running"); + return; + } + + new ShowPostTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + /* + * AsyncTask to retrieve this post from SQLite and display it + */ + private boolean mIsPostTaskRunning = false; + + private class ShowPostTask extends AsyncTask<Void, Void, Boolean> { + @Override + protected void onPreExecute() { + mIsPostTaskRunning = true; + } + + @Override + protected void onCancelled() { + mIsPostTaskRunning = false; + } + + @Override + protected Boolean doInBackground(Void... params) { + mPost = ReaderPostTable.getPost(mBlogId, mPostId, false); + if (mPost == null) { + return false; + } + + // "discover" Editor Pick posts should open the original (source) post + if (mPost.isDiscoverPost()) { + ReaderPostDiscoverData discoverData = mPost.getDiscoverData(); + if (discoverData != null + && discoverData.getDiscoverType() == ReaderPostDiscoverData.DiscoverType.EDITOR_PICK + && discoverData.getBlogId() != 0 + && discoverData.getPostId() != 0) { + mBlogId = discoverData.getBlogId(); + mPostId = discoverData.getPostId(); + mPost = ReaderPostTable.getPost(mBlogId, mPostId, false); + if (mPost == null) { + return false; + } + } + } + + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + mIsPostTaskRunning = false; + + if (!isAdded()) return; + + // make sure options menu reflects whether we now have a post + getActivity().invalidateOptionsMenu(); + + if (!result) { + // post couldn't be loaded which means it doesn't exist in db, so request it from + // the server if it hasn't already been requested + if (!mHasAlreadyRequestedPost) { + mHasAlreadyRequestedPost = true; + AppLog.i(T.READER, "reader post detail > post not found, requesting it"); + requestPost(); + } else if (!TextUtils.isEmpty(mErrorMessage)) { + // post has already been requested and failed, so restore previous error message + showError(mErrorMessage); + } + return; + } + + mReaderWebView.setIsPrivatePost(mPost.isPrivate); + mReaderWebView.setBlogSchemeIsHttps(UrlUtils.isHttps(mPost.getBlogUrl())); + + TextView txtTitle = (TextView) getView().findViewById(R.id.text_title); + TextView txtBlogName = (TextView) getView().findViewById(R.id.text_blog_name); + TextView txtDomain = (TextView) getView().findViewById(R.id.text_domain); + TextView txtDateline = (TextView) getView().findViewById(R.id.text_dateline); + TextView txtTag = (TextView) getView().findViewById(R.id.text_tag); + + WPNetworkImageView imgBlavatar = (WPNetworkImageView) getView().findViewById(R.id.image_blavatar); + WPNetworkImageView imgAvatar = (WPNetworkImageView) getView().findViewById(R.id.image_avatar); + + ViewGroup layoutHeader = (ViewGroup) getView().findViewById(R.id.layout_post_detail_header); + ReaderFollowButton followButton = (ReaderFollowButton) layoutHeader.findViewById(R.id.follow_button); + + if (!canShowFooter()) { + mLayoutFooter.setVisibility(View.GONE); + } + + // add padding to the scrollView to make room for the top and bottom toolbars - this also + // ensures the scrollbar matches the content so it doesn't disappear behind the toolbars + int topPadding = (mAutoHideToolbarListener != null ? mToolbarHeight : 0); + int bottomPadding = (canShowFooter() ? mLayoutFooter.getHeight() : 0); + mScrollView.setPadding(0, topPadding, 0, bottomPadding); + + // scrollView was hidden in onCreateView, show it now that we have the post + mScrollView.setVisibility(View.VISIBLE); + + // render the post in the webView + mRenderer = new ReaderPostRenderer(mReaderWebView, mPost); + mRenderer.beginRender(); + + txtTitle.setText(mPost.hasTitle() ? mPost.getTitle() : getString(R.string.reader_untitled_post)); + + followButton.setVisibility(mIsLoggedOutReader ? View.GONE : View.VISIBLE); + if (!mIsLoggedOutReader) { + followButton.setIsFollowed(mPost.isFollowedByCurrentUser); + followButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + togglePostFollowed(); + } + }); + } + + // clicking the header shows blog preview + if (getPostListType() != ReaderPostListType.BLOG_PREVIEW) { + layoutHeader.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ReaderActivityLauncher.showReaderBlogPreview(v.getContext(), mPost); + } + }); + } + + if (mPost.hasBlogName()) { + txtBlogName.setText(mPost.getBlogName()); + } else if (mPost.hasAuthorName()) { + txtBlogName.setText(mPost.getAuthorName()); + } else { + txtBlogName.setText(null); + } + + if (mPost.hasBlogUrl()) { + int blavatarSz = getResources().getDimensionPixelSize(R.dimen.avatar_sz_medium); + String imageUrl = GravatarUtils.blavatarFromUrl(mPost.getBlogUrl(), blavatarSz); + imgBlavatar.setImageUrl(imageUrl, WPNetworkImageView.ImageType.BLAVATAR); + txtDomain.setText(UrlUtils.getHost(mPost.getBlogUrl())); + } else { + imgBlavatar.showDefaultBlavatarImage(); + txtDomain.setText(null); + } + + if (mPost.hasPostAvatar()) { + int avatarSz = getResources().getDimensionPixelSize(R.dimen.avatar_sz_tiny); + imgAvatar.setImageUrl(mPost.getPostAvatarForDisplay(avatarSz), WPNetworkImageView.ImageType.AVATAR); + } else { + imgAvatar.showDefaultGravatarImage(); + } + + String timestamp = DateTimeUtils.javaDateToTimeSpan(mPost.getDisplayDate(), WordPress.getContext()); + if (mPost.hasAuthorName()) { + txtDateline.setText(mPost.getAuthorName() + ReaderConstants.UNICODE_BULLET_WITH_SPACE + timestamp); + } else if (mPost.hasBlogName()) { + txtDateline.setText(mPost.getBlogName() + ReaderConstants.UNICODE_BULLET_WITH_SPACE + timestamp); + } else { + txtDateline.setText(timestamp); + } + + final String tagToDisplay = mPost.getTagForDisplay(null); + if (!TextUtils.isEmpty(tagToDisplay)) { + txtTag.setText(ReaderUtils.makeHashTag(tagToDisplay)); + txtTag.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ReaderTag tag = ReaderUtils.getTagFromTagName(tagToDisplay, ReaderTagType.FOLLOWED); + ReaderActivityLauncher.showReaderTagPreview(v.getContext(), tag); + } + }); + } + + if (canShowFooter() && mLayoutFooter.getVisibility() != View.VISIBLE) { + AniUtils.fadeIn(mLayoutFooter, AniUtils.Duration.LONG); + } + + refreshIconCounts(); + } + } + + /* + * called by the web view when the content finishes loading - likes aren't displayed + * until this is triggered, to avoid having them appear before the webView content + */ + @Override + public void onPageFinished(WebView view, String url) { + if (!isAdded()) { + return; + } + + if (url != null && url.equals("about:blank")) { + // brief delay before showing comments/likes to give page time to render + view.postDelayed(new Runnable() { + @Override + public void run() { + if (!isAdded()) { + return; + } + refreshLikes(); + if (!mHasAlreadyUpdatedPost) { + mHasAlreadyUpdatedPost = true; + updatePost(); + } + requestRelatedPosts(); + } + }, 300); + } else { + AppLog.w(T.READER, "reader post detail > page finished - " + url); + } + } + + /* + * return the container view that should host the full screen video + */ + @Override + public ViewGroup onRequestCustomView() { + if (isAdded()) { + return (ViewGroup) getView().findViewById(R.id.layout_custom_view_container); + } else { + return null; + } + } + + /* + * return the container view that should be hidden when full screen video is shown + */ + @Override + public ViewGroup onRequestContentView() { + if (isAdded()) { + return (ViewGroup) getView().findViewById(R.id.layout_post_detail_container); + } else { + return null; + } + } + + @Override + public void onCustomViewShown() { + // full screen video has just been shown so hide the ActionBar + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.hide(); + } + } + + @Override + public void onCustomViewHidden() { + // user returned from full screen video so re-display the ActionBar + ActionBar actionBar = getActionBar(); + if (actionBar != null) { + actionBar.show(); + } + } + + boolean isCustomViewShowing() { + return mReaderWebView != null && mReaderWebView.isCustomViewShowing(); + } + + void hideCustomView() { + if (mReaderWebView != null) { + mReaderWebView.hideCustomView(); + } + } + + @Override + public boolean onUrlClick(String url) { + // if this is a "wordpress://blogpreview?" link, show blog preview for the blog - this is + // used for Discover posts that highlight a blog + if (ReaderUtils.isBlogPreviewUrl(url)) { + long blogId = ReaderUtils.getBlogIdFromBlogPreviewUrl(url); + if (blogId != 0) { + ReaderActivityLauncher.showReaderBlogPreview(getActivity(), blogId); + } + return true; + } + + // open YouTube videos in external app so they launch the YouTube player, open all other + // urls using an AuthenticatedWebViewActivity + final OpenUrlType openUrlType; + if (ReaderVideoUtils.isYouTubeVideoLink(url)) { + openUrlType = OpenUrlType.EXTERNAL; + } else { + openUrlType = OpenUrlType.INTERNAL; + } + ReaderActivityLauncher.openUrl(getActivity(), url, openUrlType); + return true; + } + + @Override + public boolean onImageUrlClick(String imageUrl, View view, int x, int y) { + return showPhotoViewer(imageUrl, view, x, y); + } + + private ActionBar getActionBar() { + if (isAdded() && getActivity() instanceof AppCompatActivity) { + return ((AppCompatActivity) getActivity()).getSupportActionBar(); + } else { + AppLog.w(T.READER, "reader post detail > getActionBar returned null"); + return null; + } + } + + void pauseWebView() { + if (mReaderWebView == null) { + AppLog.w(T.READER, "reader post detail > attempt to pause null webView"); + } else if (!mIsWebViewPaused) { + AppLog.d(T.READER, "reader post detail > pausing webView"); + mReaderWebView.hideCustomView(); + mReaderWebView.onPause(); + mIsWebViewPaused = true; + } + } + + void resumeWebViewIfPaused() { + if (mReaderWebView == null) { + AppLog.w(T.READER, "reader post detail > attempt to resume null webView"); + } else if (mIsWebViewPaused) { + AppLog.d(T.READER, "reader post detail > resuming paused webView"); + mReaderWebView.onResume(); + mIsWebViewPaused = false; + } + } + + @Override + public void onScrollUp(float distanceY) { + if (!mIsToolbarShowing + && -distanceY >= MIN_SCROLL_DISTANCE_Y) { + showToolbar(true); + showFooter(true); + } + } + + @Override + public void onScrollDown(float distanceY) { + if (mIsToolbarShowing + && distanceY >= MIN_SCROLL_DISTANCE_Y + && mScrollView.canScrollDown() + && mScrollView.canScrollUp() + && mScrollView.getScrollY() > mToolbarHeight) { + showToolbar(false); + showFooter(false); + } + } + + @Override + public void onScrollCompleted() { + if (!mIsToolbarShowing + && (!mScrollView.canScrollDown() || !mScrollView.canScrollUp())) { + showToolbar(true); + showFooter(true); + } + } + + private void showToolbar(boolean show) { + mIsToolbarShowing = show; + if (mAutoHideToolbarListener != null) { + mAutoHideToolbarListener.onShowHideToolbar(show); + } + } + + private void showFooter(boolean show) { + if (isAdded() && canShowFooter()) { + AniUtils.animateBottomBar(mLayoutFooter, show); + } + } + + /* + * can we show the footer bar which contains the like & comment counts? + */ + private boolean canShowFooter() { + return canShowLikeCount() || canShowCommentCount(); + } + + private boolean canShowCommentCount() { + if (mPost == null) { + return false; + } + if (mIsLoggedOutReader) { + return mPost.numReplies > 0; + } + return mPost.isWP() + && !mPost.isJetpack + && !mPost.isDiscoverPost() + && (mPost.isCommentsOpen || mPost.numReplies > 0); + } + + private boolean canShowLikeCount() { + if (mPost == null) { + return false; + } + if (mIsLoggedOutReader) { + return mPost.numLikes > 0; + } + return mPost.canLikePost() || mPost.numLikes > 0; + } + + private void setRefreshing(boolean refreshing) { + mSwipeToRefreshHelper.setRefreshing(refreshing); + } + +} |