diff --git a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java index 6727dfdc78d5..f07d8001f585 100644 --- a/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java +++ b/WordPress/src/main/java/org/wordpress/android/models/ReaderPost.java @@ -243,9 +243,21 @@ private static void assignAuthorFromJson(ReaderPost post, JSONObject jsonAuthor) post.authorName = JSONUtils.getStringDecoded(jsonAuthor, "name"); post.authorFirstName = JSONUtils.getStringDecoded(jsonAuthor, "first_name"); - post.postAvatar = JSONUtils.getString(jsonAuthor, "avatar_URL"); post.authorId = jsonAuthor.optLong("ID"); + // v1.2 endpoint contains a "has_avatar" boolean which tells us whether the author + // has a valid avatar - if this field exists and is set to false, skip setting + // the avatar URL + boolean hasAvatar; + if (jsonAuthor.has("has_avatar")) { + hasAvatar = jsonAuthor.optBoolean("has_avatar"); + } else { + hasAvatar = true; + } + if (hasAvatar) { + post.postAvatar = JSONUtils.getString(jsonAuthor, "avatar_URL"); + } + // site_URL doesn't exist for /sites/ endpoints, so get it from the author if (TextUtils.isEmpty(post.blogUrl)) { post.setBlogUrl(JSONUtils.getString(jsonAuthor, "URL")); @@ -272,6 +284,7 @@ private static void assignTagsFromJson(ReaderPost post, JSONObject jsonTags) { while (it.hasNext()) { JSONObject jsonThisTag = jsonTags.optJSONObject(it.next()); + String thisTagName = JSONUtils.getStringDecoded(jsonThisTag, "slug"); // if the number of posts on this blog that use this tag is higher than previous, // set this as the most popular tag, and set the second most popular tag to @@ -279,8 +292,10 @@ private static void assignTagsFromJson(ReaderPost post, JSONObject jsonTags) { int postCount = jsonThisTag.optInt("post_count"); if (postCount > popularCount) { nextMostPopularTag = mostPopularTag; - mostPopularTag = JSONUtils.getStringDecoded(jsonThisTag, "slug"); + mostPopularTag = thisTagName; popularCount = postCount; + } else if (nextMostPopularTag == null) { + nextMostPopularTag = thisTagName; } } @@ -461,7 +476,7 @@ public void setPrimaryTag(String tagName) { this.primaryTag = StringUtils.notNullStr(tagName); } } - boolean hasPrimaryTag() { + public boolean hasPrimaryTag() { return !TextUtils.isEmpty(primaryTag); } @@ -473,6 +488,9 @@ public void setSecondaryTag(String tagName) { this.secondaryTag = StringUtils.notNullStr(tagName); } } + public boolean hasSecondaryTag() { + return !TextUtils.isEmpty(secondaryTag); + } /* * attachments are stored as the actual JSON to avoid having a separate table for diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java index 676e40588ec5..cf2037457bd9 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderEvents.java @@ -3,7 +3,6 @@ import android.support.annotation.NonNull; import org.wordpress.android.models.ReaderPost; -import org.wordpress.android.models.ReaderPostList; import org.wordpress.android.models.ReaderTag; import org.wordpress.android.ui.reader.actions.ReaderActions; import org.wordpress.android.ui.reader.models.ReaderRelatedPostList; @@ -119,17 +118,35 @@ public ReaderActions.UpdateResult getResult() { } public static class RelatedPostsUpdated { - private final ReaderPost mSourcePost; - private final ReaderRelatedPostList mRelatedPosts; - public RelatedPostsUpdated(@NonNull ReaderPost sourcePost, @NonNull ReaderPostList relatedPosts) { - mSourcePost = sourcePost; - mRelatedPosts = new ReaderRelatedPostList(relatedPosts); - } - public ReaderPost getSourcePost() { - return mSourcePost; - } - public ReaderRelatedPostList getRelatedPosts() { - return mRelatedPosts; + private final long mSourcePostId; + private final long mSourceSiteId; + private final ReaderRelatedPostList mLocalRelatedPosts; + private final ReaderRelatedPostList mGlobalRelatedPosts; + public RelatedPostsUpdated(@NonNull ReaderPost sourcePost, + @NonNull ReaderRelatedPostList localRelatedPosts, + @NonNull ReaderRelatedPostList globalRelatedPosts) { + mSourcePostId = sourcePost.postId; + mSourceSiteId = sourcePost.blogId; + mLocalRelatedPosts = localRelatedPosts; + mGlobalRelatedPosts = globalRelatedPosts; + } + public long getSourcePostId() { + return mSourcePostId; + } + public long getSourceSiteId() { + return mSourceSiteId; + } + public ReaderRelatedPostList getLocalRelatedPosts() { + return mLocalRelatedPosts; + } + public ReaderRelatedPostList getGlobalRelatedPosts() { + return mGlobalRelatedPosts; + } + public boolean hasLocalRelatedPosts() { + return mLocalRelatedPosts.size() > 0; + } + public boolean hasGlobalRelatedPosts() { + return mGlobalRelatedPosts.size() > 0; } } } 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 index 04e10c7bbec5..532d7c4ce33a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostDetailFragment.java @@ -26,24 +26,22 @@ 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.ReaderPostDetailHeaderView; +import org.wordpress.android.ui.reader.views.ReaderRelatedPostsView; +import org.wordpress.android.ui.reader.views.ReaderTagStrip; 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; @@ -53,15 +51,11 @@ 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; @@ -366,39 +360,6 @@ private void togglePostLike() { } } - /* - * 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 */ @@ -449,8 +410,8 @@ private void replacePost(long blogId, long postId) { // 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); + getView().findViewById(R.id.related_posts_view_local).setVisibility(View.GONE); + getView().findViewById(R.id.related_posts_view_global).setVisibility(View.GONE); mLikingUsersView.setVisibility(View.GONE); mLikingUsersDivider.setVisibility(View.GONE); @@ -477,67 +438,43 @@ private void requestRelatedPosts() { } /* - * related posts were retrieved, so show them + * related posts were retrieved */ @SuppressWarnings("unused") public void onEventMainThread(ReaderEvents.RelatedPostsUpdated event) { if (!isAdded() || !hasPost()) return; - // make sure this is for the current post - if (event.getSourcePost().postId == mPost.postId && event.getSourcePost().blogId == mPost.blogId) { - showRelatedPosts(event.getRelatedPosts()); + // make sure this event is for the current post + if (event.getSourcePostId() == mPost.postId && event.getSourceSiteId() == mPost.blogId) { + if (event.hasLocalRelatedPosts()) { + showRelatedPosts(event.getLocalRelatedPosts(), false); + } + if (event.hasGlobalRelatedPosts()) { + showRelatedPosts(event.getGlobalRelatedPosts(), true); + } } } - 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); + /* + * show the passed list of related posts - can be either global (related posts from + * across wp.com) or local (related posts from the same site as the current post) + */ + private void showRelatedPosts(@NonNull ReaderRelatedPostList relatedPosts, boolean isGlobal) { + // different container views for global/local related posts + int id = isGlobal ? R.id.related_posts_view_global : R.id.related_posts_view_local; + ReaderRelatedPostsView relatedPostsView = (ReaderRelatedPostsView) getView().findViewById(id); + relatedPostsView.showRelatedPosts(relatedPosts, mPost.getBlogName(), isGlobal); + + // tapping a related posts should open the related post detail + relatedPostsView.setOnRelatedPostClickListener(new ReaderRelatedPostsView.OnRelatedPostClickListener() { + @Override + public void onRelatedPostClick(View v, long siteId, long postId) { + showRelatedPostDetail(siteId, postId); } - } + }); - 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); + if (relatedPostsView.getVisibility() != View.VISIBLE) { + AniUtils.fadeIn(relatedPostsView, AniUtils.Duration.MEDIUM); } } @@ -857,17 +794,10 @@ protected void onPostExecute(Boolean result) { 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); + ReaderTagStrip tagStrip = (ReaderTagStrip) getView().findViewById(R.id.tag_strip); + ReaderPostDetailHeaderView headerView = (ReaderPostDetailHeaderView) getView().findViewById(R.id.header_view); if (!canShowFooter()) { mLayoutFooter.setVisibility(View.GONE); } @@ -887,72 +817,12 @@ protected void onPostExecute(Boolean result) { 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); - } + 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); - } - }); - } + + headerView.setPost(mPost); + tagStrip.setPost(mPost); if (canShowFooter() && mLayoutFooter.getVisibility() != View.VISIBLE) { AniUtils.fadeIn(mLayoutFooter, AniUtils.Duration.LONG); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java index 1a2552331dba..c2feae1945d5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostRenderer.java @@ -366,9 +366,10 @@ private String formatPostContentForWebView(final String content, boolean hasTile .append(" padding: ").append(mResourceVars.marginMediumPx).append("px; }") // add a left border to blockquotes - .append(" blockquote { margin-left: ").append(mResourceVars.marginMediumPx).append("px; ") - .append(" padding-left: ").append(mResourceVars.marginMediumPx).append("px; ") - .append(" border-left: 3px solid ").append(mResourceVars.greyLightStr).append("; }") + .append(" blockquote { color: ").append(mResourceVars.greyMediumDarkStr).append("; ") + .append(" padding-left: 32px; ") + .append(" margin-left: 0px; ") + .append(" border-left: 3px solid ").append(mResourceVars.greyExtraLightStr).append("; }") // show links in the same color they are elsewhere in the app .append(" a { text-decoration: none; color: ").append(mResourceVars.linkColorStr).append("; }") diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderPostActions.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderPostActions.java index f7d66e1d2d82..48c8e6df3545 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderPostActions.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/actions/ReaderPostActions.java @@ -11,18 +11,20 @@ import com.android.volley.toolbox.StringRequest; import com.wordpress.rest.RestRequest; +import org.json.JSONArray; import org.json.JSONObject; import org.wordpress.android.WordPress; import org.wordpress.android.datasets.ReaderLikeTable; import org.wordpress.android.datasets.ReaderPostTable; import org.wordpress.android.datasets.ReaderUserTable; import org.wordpress.android.models.ReaderPost; -import org.wordpress.android.models.ReaderPostList; import org.wordpress.android.models.ReaderUserIdList; import org.wordpress.android.models.ReaderUserList; import org.wordpress.android.ui.reader.ReaderEvents; import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResult; import org.wordpress.android.ui.reader.actions.ReaderActions.UpdateResultListener; +import org.wordpress.android.ui.reader.models.ReaderRelatedPost; +import org.wordpress.android.ui.reader.models.ReaderRelatedPostList; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.JSONUtils; @@ -40,6 +42,8 @@ public class ReaderPostActions { private static final String TRACKING_REFERRER = "https://wordpress.com/"; private static final Random mRandom = new Random(); + private static final int NUM_RELATED_POSTS_TO_REQUEST = 2; + private ReaderPostActions() { throw new AssertionError(); } @@ -340,7 +344,8 @@ public Map getHeaders() throws AuthFailureError { } /* - * request posts related to the passed one + * request posts related to the passed one, endpoint returns a combined list of related posts + * posts from across wp.com and related posts from the same site as the passed post */ public static void requestRelatedPosts(final ReaderPost sourcePost) { if (sourcePost == null) return; @@ -360,20 +365,36 @@ public void onErrorResponse(VolleyError volleyError) { } }; - String path = "/read/site/" + sourcePost.blogId + "/post/" + sourcePost.postId + "/related"; + String path = "/read/site/" + sourcePost.blogId + + "/post/" + sourcePost.postId + + "/related" + + "?size_local=" + NUM_RELATED_POSTS_TO_REQUEST + + "&size_global=" + NUM_RELATED_POSTS_TO_REQUEST + + "&fields=" + ReaderRelatedPost.RELATED_POST_FIELDS; WordPress.getRestClientUtilsV1_2().get(path, null, null, listener, errorListener); } - private static void handleRelatedPostsResponse(final ReaderPost sourcePost, final JSONObject jsonObject) { + private static void handleRelatedPostsResponse(final ReaderPost sourcePost, + final JSONObject jsonObject) { if (jsonObject == null) return; new Thread() { @Override public void run() { - ReaderPostList relatedPosts = ReaderPostList.fromJson(jsonObject); - if (relatedPosts != null && relatedPosts.size() > 0) { - ReaderPostTable.addOrUpdatePosts(null, relatedPosts); - EventBus.getDefault().post(new ReaderEvents.RelatedPostsUpdated(sourcePost, relatedPosts)); + JSONArray jsonPosts = jsonObject.optJSONArray("posts"); + if (jsonPosts != null) { + ReaderRelatedPostList allRelatedPosts = ReaderRelatedPostList.fromJsonPosts(jsonPosts); + // split into posts from the passed site (local) and from across wp.com (global) + ReaderRelatedPostList localRelatedPosts = new ReaderRelatedPostList(); + ReaderRelatedPostList globalRelatedPosts = new ReaderRelatedPostList(); + for (ReaderRelatedPost relatedPost : allRelatedPosts) { + if (relatedPost.getSiteId() == sourcePost.blogId) { + localRelatedPosts.add(relatedPost); + } else { + globalRelatedPosts.add(relatedPost); + } + } + EventBus.getDefault().post(new ReaderEvents.RelatedPostsUpdated(sourcePost, localRelatedPosts, globalRelatedPosts)); } } }.start(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPost.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPost.java index e959689087b3..be93047bfe5a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPost.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPost.java @@ -1,86 +1,123 @@ package org.wordpress.android.ui.reader.models; -import android.support.annotation.NonNull; import android.text.TextUtils; -import org.wordpress.android.models.ReaderPost; -import org.wordpress.android.util.UrlUtils; +import org.json.JSONException; +import org.json.JSONObject; +import org.wordpress.android.util.AppLog; +import org.wordpress.android.util.HtmlUtils; +import org.wordpress.android.util.JSONUtils; /** * simplified version of a reader post that contains only the fields necessary for a related post */ public class ReaderRelatedPost { - private final long mPostId; - private final long mBlogId; - private final String mTitle; - private final String mByline; - private final String mFeaturedImage; - - public ReaderRelatedPost(@NonNull ReaderPost post) { - mPostId = post.postId; - mBlogId = post.blogId; - - mTitle = post.getTitle(); - mFeaturedImage = post.getFeaturedImage(); - - /* - * we want to include the blog name in the byline when it's available, and most sites - * will have a name, but in rare cases there isn't one so we show the domain instead - */ - String blogNameOrDomain; - boolean hasBlogNameOrDomain; - if (post.hasBlogName()) { - blogNameOrDomain = post.getBlogName(); - hasBlogNameOrDomain = true; - } else if (post.hasBlogUrl()) { - blogNameOrDomain = UrlUtils.getHost(post.getBlogUrl()); - hasBlogNameOrDomain = true; - } else { - blogNameOrDomain = null; - hasBlogNameOrDomain = false; + private long mPostId; + private long mSiteId; + private boolean mIsFollowing; + + private String mTitle; + private String mAuthorName; + private String mAuthorAvatarUrl; + private String mExcerpt; + private String mSiteName; + private String mFeaturedImageUrl; + + // these are the specific fields we should ask for when requesting related posts from + // the endpoint - note that we want to avoid ever requesting the post content, since + // that makes the call much heavier + public static final String RELATED_POST_FIELDS = "ID,site_ID,title,excerpt,site_name,is_following,author,featured_image,featured_media"; + + public static ReaderRelatedPost fromJson(JSONObject json) { + if (json == null) { + throw new IllegalArgumentException("ReaderRelatedPost requires a non-null JSONObject"); } - /* - * The byline should show the author name and blog name if both are available, but if - * they're the same (which happens frequently) we only need to show the blog name. - * Otherwise, show either the blog name or author name depending on which is available. - */ - if (post.hasAuthorName() && hasBlogNameOrDomain) { - if (post.getAuthorName().equalsIgnoreCase(blogNameOrDomain)) { - mByline = blogNameOrDomain; - } else { - mByline = post.getAuthorName() + ", " + blogNameOrDomain; + ReaderRelatedPost post = new ReaderRelatedPost(); + + // ID and site_ID are required, so make sure we have them + try { + post.mPostId = json.getLong("ID"); + post.mSiteId = json.getLong("site_ID"); + } catch (JSONException e) { + AppLog.e(AppLog.T.READER, e); + return null; + } + + post.mIsFollowing = JSONUtils.getBool(json, "is_following"); + + post.mTitle = JSONUtils.getStringDecoded(json, "title"); + post.mExcerpt = HtmlUtils.fastStripHtml(JSONUtils.getString(json, "excerpt")).trim(); + post.mSiteName = JSONUtils.getStringDecoded(json, "site_name"); + post.mFeaturedImageUrl = JSONUtils.getString(json, "featured_image"); + + JSONObject jsonAuthor = json.optJSONObject("author"); + if (jsonAuthor != null) { + post.mAuthorName = JSONUtils.getStringDecoded(jsonAuthor, "name"); + post.mAuthorAvatarUrl = JSONUtils.getString(jsonAuthor, "avatar_URL"); + } + + // if there's no featured image, check if featured media has been set to an image + if (!post.hasFeaturedImageUrl() && json.has("featured_media")) { + JSONObject jsonMedia = json.optJSONObject("featured_media"); + String type = JSONUtils.getString(jsonMedia, "type"); + if (type.equals("image")) { + post.mFeaturedImageUrl = JSONUtils.getString(jsonMedia, "uri"); } - } else if (post.hasAuthorName()) { - mByline = post.getAuthorName(); - } else if (hasBlogNameOrDomain) { - mByline = blogNameOrDomain; - } else { - mByline = ""; } + + return post; } public long getPostId() { return mPostId; } - public long getBlogId() { - return mBlogId; + public long getSiteId() { + return mSiteId; } public String getTitle() { return mTitle; } - public String getByline() { - return mByline; + public String getExcerpt() { + return mExcerpt; + } + + public String getSiteName() { + return mSiteName; + } + + public String getAuthorName() { + return mAuthorName; + } + + public String getAuthorAvatarUrl() { + return mAuthorAvatarUrl; + } + + public String getFeaturedImageUrl() { + return mFeaturedImageUrl; + } + + public boolean isFollowing() { + return mIsFollowing; + } + + public void setIsFollowing(boolean isFollowing) { + mIsFollowing = isFollowing; + } + + public boolean hasExcerpt() { + return !TextUtils.isEmpty(mExcerpt); } - public String getFeaturedImage() { - return mFeaturedImage; + public boolean hasAuthorAvatarUrl() { + return !TextUtils.isEmpty(mAuthorAvatarUrl); } - public boolean hasFeaturedImage() { - return !TextUtils.isEmpty(mFeaturedImage); + public boolean hasFeaturedImageUrl() { + return !TextUtils.isEmpty(mFeaturedImageUrl); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPostList.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPostList.java index 94c1d5b5aace..91476d64f540 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPostList.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/models/ReaderRelatedPostList.java @@ -2,17 +2,25 @@ import android.support.annotation.NonNull; -import org.wordpress.android.models.ReaderPost; -import org.wordpress.android.models.ReaderPostList; +import org.json.JSONArray; +import org.json.JSONObject; import java.util.ArrayList; public class ReaderRelatedPostList extends ArrayList { - public ReaderRelatedPostList(@NonNull ReaderPostList posts) { - for (ReaderPost post: posts) { - add(new ReaderRelatedPost(post)); + public static ReaderRelatedPostList fromJsonPosts(@NonNull JSONArray jsonPosts) { + ReaderRelatedPostList posts = new ReaderRelatedPostList(); + for (int i = 0; i < jsonPosts.length(); i++) { + JSONObject jsonRelatedPost = jsonPosts.optJSONObject(i); + if (jsonRelatedPost != null) { + ReaderRelatedPost relatedPost = ReaderRelatedPost.fromJson(jsonRelatedPost); + if (relatedPost != null) { + posts.add(relatedPost); + } + } } + return posts; } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.java index efd5484b82c6..6028c5a9796b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderFollowButton.java @@ -6,6 +6,7 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; +import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; @@ -22,32 +23,65 @@ public class ReaderFollowButton extends LinearLayout { private TextView mTextFollow; private ImageView mImageFollow; private boolean mIsFollowed; + private boolean mShowCaption; public ReaderFollowButton(Context context){ super(context); - initView(context); + initView(context, null); } public ReaderFollowButton(Context context, AttributeSet attrs) { super(context, attrs); - initView(context); + initView(context, attrs); } public ReaderFollowButton(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); - initView(context); + initView(context, attrs); } - private void initView(Context context) { + private void initView(Context context, AttributeSet attrs) { inflate(context, R.layout.reader_follow_button, this); mTextFollow = (TextView) findViewById(R.id.text_follow_button); mImageFollow = (ImageView) findViewById(R.id.image_follow_button); + + // default to showing caption, then read the value from passed attributes + mShowCaption = true; + if (attrs != null) { + TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ReaderFollowButton, 0, 0); + if (array != null) { + mShowCaption = array.getBoolean(R.styleable.ReaderFollowButton_wpShowFollowButtonCaption, true); + } + } + + // hide follow text, enlarge the follow icon, and make the icon blue if there's no caption + if (!mShowCaption) { + mTextFollow.setText(null); + mTextFollow.setVisibility(View.GONE); + int iconSz = context.getResources().getDimensionPixelSize(R.dimen.reader_follow_icon_no_caption); + mImageFollow.getLayoutParams().width = iconSz; + mImageFollow.getLayoutParams().height = iconSz; + mImageFollow.setImageResource(R.drawable.reader_follow_blue); + } } private void updateFollowText() { + if (mShowCaption) { + mTextFollow.setText(mIsFollowed ? R.string.reader_btn_unfollow : R.string.reader_btn_follow); + } mTextFollow.setSelected(mIsFollowed); - mTextFollow.setText(mIsFollowed ? R.string.reader_btn_unfollow : R.string.reader_btn_follow); - mImageFollow.setImageResource(mIsFollowed ? R.drawable.reader_following : R.drawable.reader_follow); + + // show green icon if site is followed, gray icon if not followed and there's a caption, + // blue icon if not followed and there is no caption + int drawableId; + if (mIsFollowed) { + drawableId = R.drawable.reader_following; + } else if (mShowCaption) { + drawableId = R.drawable.reader_follow; + } else { + drawableId = R.drawable.reader_follow_blue; + } + mImageFollow.setImageResource(drawableId); } @Override diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderIconCountView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderIconCountView.java index 9e2ff2fd2e97..9a5566264d6e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderIconCountView.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderIconCountView.java @@ -10,7 +10,6 @@ import org.wordpress.android.R; import org.wordpress.android.ui.reader.utils.ReaderUtils; -import org.wordpress.android.util.FormatUtils; /* * used when showing comment + comment count, like + like count @@ -86,6 +85,13 @@ public void setEnabled(boolean enabled) { } public void setCount(int count) { - mTextCount.setText(FormatUtils.formatInt(count)); + switch (mIconType) { + case ICON_LIKE: + mTextCount.setText(ReaderUtils.getShortLikeLabelText(getContext(), count)); + break; + case ICON_COMMENT: + mTextCount.setText(ReaderUtils.getShortCommentLabelText(getContext(), count)); + break; + } } } \ No newline at end of file diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailHeaderView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailHeaderView.java new file mode 100644 index 000000000000..29da3180f210 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderPostDetailHeaderView.java @@ -0,0 +1,249 @@ +package org.wordpress.android.ui.reader.views; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.datasets.ReaderBlogTable; +import org.wordpress.android.models.ReaderBlog; +import org.wordpress.android.models.ReaderPost; +import org.wordpress.android.ui.reader.ReaderActivityLauncher; +import org.wordpress.android.ui.reader.actions.ReaderActions; +import org.wordpress.android.ui.reader.actions.ReaderBlogActions; +import org.wordpress.android.ui.reader.utils.ReaderUtils; +import org.wordpress.android.util.AppLog; +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.widgets.WPNetworkImageView; + +/** + * topmost view in post detail - shows blavatar + avatar, author name, blog name, and follow button + */ +public class ReaderPostDetailHeaderView extends LinearLayout { + + private ReaderPost mPost; + private ReaderFollowButton mFollowButton; + + public ReaderPostDetailHeaderView(Context context) { + super(context); + initView(context); + } + + public ReaderPostDetailHeaderView(Context context, AttributeSet attrs) { + super(context, attrs); + initView(context); + } + + public ReaderPostDetailHeaderView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(context); + } + + private void initView(Context context) { + View view = inflate(context, R.layout.reader_post_detail_header_view, this); + mFollowButton = (ReaderFollowButton) view.findViewById(R.id.header_follow_button); + } + + public void setPost(@NonNull ReaderPost post) { + mPost = post; + + TextView txtTitle = (TextView) findViewById(R.id.text_header_title); + TextView txtSubtitle = (TextView) findViewById(R.id.text_header_subtitle); + + boolean hasBlogName = mPost.hasBlogName(); + boolean hasAuthorName = mPost.hasAuthorName(); + + if (hasBlogName && hasAuthorName) { + // don't show author name if it's the same as the blog name + if (mPost.getAuthorName().equals(mPost.getBlogName())) { + txtTitle.setText(mPost.getAuthorName()); + txtSubtitle.setVisibility(View.GONE); + } else { + txtTitle.setText(mPost.getAuthorName()); + txtSubtitle.setText(mPost.getBlogName()); + } + } else if (hasBlogName) { + txtTitle.setText(mPost.getBlogName()); + txtSubtitle.setVisibility(View.GONE); + } else if (hasAuthorName) { + txtTitle.setText(mPost.getAuthorName()); + txtSubtitle.setVisibility(View.GONE); + } else { + txtTitle.setText(R.string.untitled); + txtSubtitle.setVisibility(View.GONE); + } + + // show blog preview when these views are tapped + txtTitle.setOnClickListener(mClickListener); + txtSubtitle.setOnClickListener(mClickListener); + + if (ReaderUtils.isLoggedOutReader()) { + mFollowButton.setVisibility(View.GONE); + } else { + mFollowButton.setVisibility(View.VISIBLE); + mFollowButton.setIsFollowed(mPost.isFollowedByCurrentUser); + mFollowButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + toggleFollowStatus(); + } + }); + } + + // get local blog info so we can set the follower count and blavatar + ReaderBlog blogInfo = mPost.isExternal ? ReaderBlogTable.getFeedInfo(mPost.feedId) : ReaderBlogTable.getBlogInfo(mPost.blogId); + if (blogInfo != null) { + showBlogInfo(blogInfo); + } + + // update blog info if it's time or it doesn't exist + if (blogInfo == null || ReaderBlogTable.isTimeToUpdateBlogInfo(blogInfo)) { + updateBlogInfo(); + } + } + + /* + * get the latest info about this post's blog so we have an accurate follower count + */ + private void updateBlogInfo() { + if (!NetworkUtils.isNetworkAvailable(getContext())) return; + + ReaderActions.UpdateBlogInfoListener listener = new ReaderActions.UpdateBlogInfoListener() { + @Override + public void onResult(ReaderBlog blogInfo) { + showBlogInfo(blogInfo); + } + }; + if (mPost.isExternal) { + ReaderBlogActions.updateFeedInfo(mPost.feedId, null, listener); + } else { + ReaderBlogActions.updateBlogInfo(mPost.blogId, null, listener); + } + } + + private void showBlogInfo(ReaderBlog blogInfo) { + String blavatarUrl = blogInfo != null ? blogInfo.getImageUrl() : null; + String avatarUrl = mPost != null ? mPost.getPostAvatar() : null; + showBlavatarAndAvatar(blavatarUrl, avatarUrl); + } + + private void showBlavatarAndAvatar(String blavatarUrl, String avatarUrl) { + boolean hasBlavatar = !TextUtils.isEmpty(blavatarUrl); + boolean hasAvatar = !TextUtils.isEmpty(avatarUrl); + + AppLog.w(AppLog.T.READER, avatarUrl); + + int frameSize = getResources().getDimensionPixelSize(R.dimen.reader_detail_header_avatar_frame); + + View avatarFrame = findViewById(R.id.frame_avatar); + WPNetworkImageView imgBlavatar = (WPNetworkImageView) findViewById(R.id.image_header_blavatar); + WPNetworkImageView imgAvatar = (WPNetworkImageView) findViewById(R.id.image_header_avatar); + + /* + * - if there's a blavatar and an avatar, show both of them overlaid using default sizing + * - if there's only a blavatar, show it the full size of the parent frame and hide the avatar + * - if there's only an avatar, show it the full size of the parent frame and hide the blavatar + * - if there's neither a blavatar nor an avatar, hide them both + */ + if (hasBlavatar && hasAvatar) { + int blavatarSz = getResources().getDimensionPixelSize(R.dimen.reader_detail_header_blavatar); + imgBlavatar.getLayoutParams().height = blavatarSz; + imgBlavatar.getLayoutParams().width = blavatarSz; + imgBlavatar.setImageUrl( + PhotonUtils.getPhotonImageUrl(blavatarUrl, blavatarSz, blavatarSz), + WPNetworkImageView.ImageType.BLAVATAR); + imgBlavatar.setVisibility(View.VISIBLE); + + int avatarSz = getResources().getDimensionPixelSize(R.dimen.reader_detail_header_avatar); + imgAvatar.getLayoutParams().height = avatarSz; + imgAvatar.getLayoutParams().width = avatarSz; + imgAvatar.setImageUrl( + GravatarUtils.fixGravatarUrl(avatarUrl, avatarSz), + WPNetworkImageView.ImageType.AVATAR); + imgAvatar.setVisibility(View.VISIBLE); + } else if (hasBlavatar) { + imgBlavatar.getLayoutParams().height = frameSize; + imgBlavatar.getLayoutParams().width = frameSize; + imgBlavatar.setImageUrl( + PhotonUtils.getPhotonImageUrl(blavatarUrl, frameSize, frameSize), + WPNetworkImageView.ImageType.BLAVATAR); + imgBlavatar.setVisibility(View.VISIBLE); + + imgAvatar.setVisibility(View.GONE); + } else if (hasAvatar) { + imgBlavatar.setVisibility(View.GONE); + + imgAvatar.getLayoutParams().height = frameSize; + imgAvatar.getLayoutParams().width = frameSize; + imgAvatar.setImageUrl( + GravatarUtils.fixGravatarUrl(avatarUrl, frameSize), + WPNetworkImageView.ImageType.AVATAR); + imgAvatar.setVisibility(View.VISIBLE); + } else { + imgBlavatar.setVisibility(View.GONE); + imgAvatar.setVisibility(View.GONE); + } + + // hide the frame if there's neither a blavatar nor an avatar + avatarFrame.setVisibility(hasAvatar || hasBlavatar ? View.VISIBLE : View.GONE); + + imgBlavatar.setOnClickListener(mClickListener); + imgAvatar.setOnClickListener(mClickListener); + } + + /* + * click listener which shows blog preview + */ + private final OnClickListener mClickListener = new OnClickListener() { + @Override + public void onClick(View v) { + if (mPost != null) { + ReaderActivityLauncher.showReaderBlogPreview(v.getContext(), mPost); + } + } + }; + + private void toggleFollowStatus() { + if (!NetworkUtils.checkConnection(getContext())) return; + + final boolean isAskingToFollow = !mPost.isFollowedByCurrentUser; + + ReaderActions.ActionListener listener = new ReaderActions.ActionListener() { + @Override + public void onActionResult(boolean succeeded) { + if (getContext() == null) return; + + mFollowButton.setEnabled(true); + if (succeeded) { + mPost.isFollowedByCurrentUser = isAskingToFollow; + } else { + int errResId = isAskingToFollow ? R.string.reader_toast_err_follow_blog : R.string.reader_toast_err_unfollow_blog; + ToastUtils.showToast(getContext(), errResId); + mFollowButton.setIsFollowed(!isAskingToFollow); + } + updateBlogInfo(); + } + }; + + // disable follow button until API call returns + mFollowButton.setEnabled(false); + + boolean result; + if (mPost.isExternal) { + result = ReaderBlogActions.followFeedById(mPost.feedId, isAskingToFollow, listener); + } else { + result = ReaderBlogActions.followBlogById(mPost.blogId, isAskingToFollow, listener); + } + + if (result) { + mFollowButton.setIsFollowedAnimated(isAskingToFollow); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderRelatedPostsView.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderRelatedPostsView.java new file mode 100644 index 000000000000..78f2981da2c2 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderRelatedPostsView.java @@ -0,0 +1,225 @@ +package org.wordpress.android.ui.reader.views; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.ui.reader.actions.ReaderActions; +import org.wordpress.android.ui.reader.actions.ReaderBlogActions; +import org.wordpress.android.ui.reader.models.ReaderRelatedPost; +import org.wordpress.android.ui.reader.models.ReaderRelatedPostList; +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.widgets.WPNetworkImageView; + +/** + * used by the detail view to display related posts, which can be either local (related posts + * from the same site as the source post) or global (related posts from across wp.com) + */ +public class ReaderRelatedPostsView extends LinearLayout { + + public interface OnRelatedPostClickListener { + void onRelatedPostClick(View v, long siteId, long postId); + } + + private OnRelatedPostClickListener mClickListener; + private int mFeaturedImageWidth; + private ReaderRelatedPostList mRelatedPostList; + + public ReaderRelatedPostsView(Context context) { + super(context); + initView(context); + } + + public ReaderRelatedPostsView(Context context, AttributeSet attrs) { + super(context, attrs); + initView(context); + } + + public ReaderRelatedPostsView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(context); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public ReaderRelatedPostsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initView(context); + } + + private void initView(Context context) { + inflate(context, R.layout.reader_related_posts_view, this); + mFeaturedImageWidth = DisplayUtils.dpToPx(context, context.getResources().getDimensionPixelSize(R.dimen.reader_related_post_image_width)); + } + + public void setOnRelatedPostClickListener(OnRelatedPostClickListener listener) { + mClickListener = listener; + } + + public void showRelatedPosts(@NonNull ReaderRelatedPostList posts, String siteName, boolean isGlobal) { + mRelatedPostList = posts; + + ViewGroup container = (ViewGroup) findViewById(R.id.container_related_posts); + container.removeAllViews(); + + // nothing more to do if passed list is empty + if (mRelatedPostList.size() == 0) return; + + int avatarSize = DisplayUtils.dpToPx(getContext(), getResources().getDimensionPixelSize(R.dimen.avatar_sz_extra_small)); + + // add a separate view for each related post + LayoutInflater inflater = LayoutInflater.from(getContext()); + for (int index = 0; index < mRelatedPostList.size(); index++) { + final int position = index; + ReaderRelatedPost relatedPost = mRelatedPostList.get(position); + + View postView = inflater.inflate(R.layout.reader_related_post, container, false); + TextView txtTitle = (TextView) postView.findViewById(R.id.text_related_post_title); + TextView txtExcerpt = (TextView) postView.findViewById(R.id.text_related_post_excerpt); + View siteHeader = postView.findViewById(R.id.layout_related_post_site_header); + + txtTitle.setText(relatedPost.getTitle()); + + if (relatedPost.hasExcerpt()) { + txtExcerpt.setText(relatedPost.getExcerpt()); + txtExcerpt.setVisibility(View.VISIBLE); + } else { + txtExcerpt.setVisibility(View.GONE); + } + + // site header only appears for global related posts + if (isGlobal) { + WPNetworkImageView imgAvatar = (WPNetworkImageView) siteHeader.findViewById(R.id.image_avatar); + TextView txtSiteName = (TextView) siteHeader.findViewById(R.id.text_site_name); + TextView txtAuthorName = (TextView) siteHeader.findViewById(R.id.text_author_name); + txtSiteName.setText(relatedPost.getSiteName()); + txtAuthorName.setText(relatedPost.getAuthorName()); + if (relatedPost.hasAuthorAvatarUrl()) { + String avatarUrl = GravatarUtils.fixGravatarUrl(relatedPost.getAuthorAvatarUrl(), avatarSize); + imgAvatar.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR); + } else { + imgAvatar.showDefaultGravatarImage(); + } + + final ReaderFollowButton btnFollow = (ReaderFollowButton) siteHeader.findViewById(R.id.related_post_follow_button); + btnFollow.setIsFollowed(relatedPost.isFollowing()); + btnFollow.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + toggleFollowStatus(btnFollow, position); + } + }); + + siteHeader.setVisibility(View.VISIBLE); + } else { + siteHeader.setVisibility(View.GONE); + } + + showFeaturedImage(postView, relatedPost); + + postView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mClickListener != null) { + mClickListener.onRelatedPostClick(view, + mRelatedPostList.get(position).getSiteId(), + mRelatedPostList.get(position).getPostId()); + } + } + }); + + container.addView(postView); + } + + // make sure the label for these related posts has the correct caption + TextView label = (TextView) findViewById(R.id.text_related_posts_label); + if (isGlobal) { + label.setText(getContext().getString(R.string.reader_label_global_related_posts)); + } else { + label.setText(String.format(getContext().getString(R.string.reader_label_local_related_posts), siteName)); + } + } + + /** + * user tapped follow button on a global related post + * @param btnFollow Follow button for the site to follow + * @param position index of the related post in mRelatedPosts + */ + private void toggleFollowStatus(final ReaderFollowButton btnFollow, final int position) { + if (!NetworkUtils.checkConnection(getContext())) return; + + final boolean isAskingToFollow = !mRelatedPostList.get(position).isFollowing(); + + ReaderActions.ActionListener listener = new ReaderActions.ActionListener() { + @Override + public void onActionResult(boolean succeeded) { + if (getContext() == null) return; + + btnFollow.setEnabled(true); + if (succeeded) { + mRelatedPostList.get(position).setIsFollowing(isAskingToFollow); + } else { + int errResId = isAskingToFollow ? R.string.reader_toast_err_follow_blog : R.string.reader_toast_err_unfollow_blog; + ToastUtils.showToast(getContext(), errResId); + btnFollow.setIsFollowed(!isAskingToFollow); + } + } + }; + + // disable follow button until call completes + btnFollow.setEnabled(false); + + ReaderBlogActions.followBlogById(mRelatedPostList.get(position).getSiteId(), isAskingToFollow, listener); + btnFollow.setIsFollowedAnimated(isAskingToFollow); + } + + /** + * shows the featured image for the passed related post, if available + * @param postView parent view which contains the featured image and other related post views + * @param relatedPost related post to operate on + */ + private void showFeaturedImage(final View postView, final ReaderRelatedPost relatedPost) { + final WPNetworkImageView imgFeatured = (WPNetworkImageView) postView.findViewById(R.id.image_featured); + + // post must have an excerpt in order to show featured image (not enough space otherwise) + if (!relatedPost.hasFeaturedImageUrl() || !relatedPost.hasExcerpt()) { + imgFeatured.setVisibility(View.GONE); + return; + } + + // featured image has height set to MATCH_PARENT so wait for parent view's layout to complete + // before loading image so we can set the image height correctly, then tell the imageView + // to crop the downloaded image to fit the exact width/height of the view + postView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + postView.getViewTreeObserver().removeOnGlobalLayoutListener(this); + int cropWidth = mFeaturedImageWidth; + int cropHeight = postView.getHeight(); + String photonUrl = PhotonUtils.getPhotonImageUrl( + relatedPost.getFeaturedImageUrl(), cropWidth, cropHeight); + imgFeatured.setImageUrl( + photonUrl, + WPNetworkImageView.ImageType.PHOTO, + null, + cropWidth, + cropHeight); + } + }); + + imgFeatured.setVisibility(View.VISIBLE); + } + +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderTagStrip.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderTagStrip.java new file mode 100644 index 000000000000..f4ad2559134a --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/views/ReaderTagStrip.java @@ -0,0 +1,95 @@ +package org.wordpress.android.ui.reader.views; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.wordpress.android.R; +import org.wordpress.android.models.ReaderPost; +import org.wordpress.android.models.ReaderTag; +import org.wordpress.android.models.ReaderTagType; +import org.wordpress.android.ui.reader.ReaderActivityLauncher; +import org.wordpress.android.ui.reader.utils.ReaderUtils; +import org.wordpress.android.util.AniUtils; + +/** + * used by the detail view to display the primary and secondary tags from a reader post + */ +public class ReaderTagStrip extends LinearLayout { + + private LinearLayout mView; + private int mNumTags; + + public ReaderTagStrip(Context context) { + super(context); + initView(context); + } + + public ReaderTagStrip(Context context, AttributeSet attrs) { + super(context, attrs); + initView(context); + } + + public ReaderTagStrip(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(context); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public ReaderTagStrip(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + initView(context); + } + + private void initView(Context context) { + mView = (LinearLayout) inflate(context, R.layout.reader_tag_strip, this); + } + + public void setPost(@NonNull ReaderPost post) { + if (mView.getChildCount() > 0) { + mView.removeAllViews(); + } + + mNumTags = 0; + + if (post.hasPrimaryTag()) { + addTag(post.getPrimaryTag()); + } + if (post.hasSecondaryTag()) { + addTag(post.getSecondaryTag()); + } + + if (mNumTags == 0) { + mView.setVisibility(View.GONE); + } else if (mView.getVisibility() != View.VISIBLE) { + AniUtils.fadeIn(mView, AniUtils.Duration.SHORT); + } + } + + private void addTag(@NonNull final String tagName) { + String tagDisplayName = mNumTags > 0 ? ", " + ReaderUtils.makeHashTag(tagName) : ReaderUtils.makeHashTag(tagName); + + // inflate a new textView to show this tag + LayoutInflater inflater = LayoutInflater.from(getContext()); + TextView txtTag = (TextView) inflater.inflate(R.layout.reader_tag_strip_label, mView, false); + txtTag.setText(tagDisplayName); + mView.addView(txtTag); + + mNumTags++; + + // show all posts with this tag when clicked + txtTag.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + ReaderTag tag = ReaderUtils.createTagFromTagName(tagName, ReaderTagType.FOLLOWED); + ReaderActivityLauncher.showReaderTagPreview(v.getContext(), tag); + } + }); + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/widgets/WPNetworkImageView.java b/WordPress/src/main/java/org/wordpress/android/widgets/WPNetworkImageView.java index 1f858664c534..05f7eaf61d65 100644 --- a/WordPress/src/main/java/org/wordpress/android/widgets/WPNetworkImageView.java +++ b/WordPress/src/main/java/org/wordpress/android/widgets/WPNetworkImageView.java @@ -6,6 +6,7 @@ import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; +import android.media.ThumbnailUtils; import android.os.AsyncTask; import android.support.annotation.ColorRes; import android.support.annotation.DrawableRes; @@ -60,6 +61,9 @@ public interface ImageLoadListener { private int mDefaultImageResId; private int mErrorImageResId; + private int mCropWidth; + private int mCropHeight; + private static final HashSet mUrlSkipList = new HashSet<>(); public WPNetworkImageView(Context context) { @@ -77,9 +81,25 @@ public void setImageUrl(String url, ImageType imageType) { } public void setImageUrl(String url, ImageType imageType, ImageLoadListener imageLoadListener) { + setImageUrl(url, imageType, imageLoadListener, 0, 0); + } + + public void setImageUrl(String url, + ImageType imageType, + ImageLoadListener imageLoadListener, + int cropWidth, + int cropHeight) { mUrl = url; mImageType = imageType; + if (cropWidth > 0 && cropHeight > 0) { + mCropWidth = cropWidth; + mCropHeight = cropHeight; + } else { + mCropWidth = 0; + mCropHeight = 0; + } + // The URL has potentially changed. See if we need to load it. loadImageIfNecessary(false, imageLoadListener); } @@ -258,6 +278,11 @@ private void handleResponse(ImageLoader.ImageContainer response, boolean isCache setVisibility(View.VISIBLE); } + // if cropping is requested, do it before further manipulation + if (mCropWidth > 0 && mCropHeight > 0) { + bitmap = ThumbnailUtils.extractThumbnail(bitmap, mCropWidth, mCropHeight); + } + // Apply circular rounding to avatars in a background task if (mImageType == ImageType.AVATAR) { new ShapeBitmapTask(ShapeType.CIRCLE, imageLoadListener).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, bitmap); diff --git a/WordPress/src/main/res/drawable/reader_follow_blue.xml b/WordPress/src/main/res/drawable/reader_follow_blue.xml new file mode 100644 index 000000000000..fcee9d5e7ad0 --- /dev/null +++ b/WordPress/src/main/res/drawable/reader_follow_blue.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/WordPress/src/main/res/drawable/reader_related_posts_background.xml b/WordPress/src/main/res/drawable/reader_related_posts_background.xml deleted file mode 100644 index f4a6cbd94434..000000000000 --- a/WordPress/src/main/res/drawable/reader_related_posts_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/WordPress/src/main/res/drawable/shape_oval_white.xml b/WordPress/src/main/res/drawable/shape_oval_white.xml new file mode 100644 index 000000000000..b490394610a5 --- /dev/null +++ b/WordPress/src/main/res/drawable/shape_oval_white.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/WordPress/src/main/res/layout/reader_follow_button.xml b/WordPress/src/main/res/layout/reader_follow_button.xml index d6c90544ccfa..c9262d7a06fe 100644 --- a/WordPress/src/main/res/layout/reader_follow_button.xml +++ b/WordPress/src/main/res/layout/reader_follow_button.xml @@ -5,7 +5,6 @@ android:id="@+id/frame_follow_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="?android:selectableItemBackground" android:orientation="horizontal" android:paddingBottom="@dimen/margin_small" android:paddingLeft="@dimen/margin_small" diff --git a/WordPress/src/main/res/layout/reader_fragment_post_detail.xml b/WordPress/src/main/res/layout/reader_fragment_post_detail.xml index 90fa6054ad01..a3abeb5adcb5 100644 --- a/WordPress/src/main/res/layout/reader_fragment_post_detail.xml +++ b/WordPress/src/main/res/layout/reader_fragment_post_detail.xml @@ -28,8 +28,8 @@ android:layout_height="wrap_content" android:paddingTop="@dimen/margin_large"> - - - + android:layout_below="@+id/layout_post_detail_content" + android:orientation="vertical"> + + + + + + - - - - - - - @@ -89,4 +63,30 @@ android:visibility="gone" tools:visibility="visible" /> + + + + + + + \ No newline at end of file diff --git a/WordPress/src/main/res/layout/reader_include_post_detail_footer.xml b/WordPress/src/main/res/layout/reader_include_post_detail_footer.xml index 023caf29cc7e..55f0baaded04 100644 --- a/WordPress/src/main/res/layout/reader_include_post_detail_footer.xml +++ b/WordPress/src/main/res/layout/reader_include_post_detail_footer.xml @@ -27,21 +27,6 @@ android:layout_marginRight="@dimen/reader_detail_margin" android:layout_marginTop="@dimen/margin_medium"> - - diff --git a/WordPress/src/main/res/layout/reader_include_post_detail_header.xml b/WordPress/src/main/res/layout/reader_include_post_detail_header.xml deleted file mode 100644 index 8c7c84bddc9b..000000000000 --- a/WordPress/src/main/res/layout/reader_include_post_detail_header.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/WordPress/src/main/res/layout/reader_post_detail_header_view.xml b/WordPress/src/main/res/layout/reader_post_detail_header_view.xml new file mode 100644 index 000000000000..18f39b1c54bb --- /dev/null +++ b/WordPress/src/main/res/layout/reader_post_detail_header_view.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/reader_related_post.xml b/WordPress/src/main/res/layout/reader_related_post.xml index 6f64e009f5eb..6540b20835a7 100644 --- a/WordPress/src/main/res/layout/reader_related_post.xml +++ b/WordPress/src/main/res/layout/reader_related_post.xml @@ -1,51 +1,109 @@ + + + + android:layout_marginBottom="@dimen/margin_extra_large" + android:background="?android:selectableItemBackground"> + + - + android:layout_marginBottom="@dimen/margin_medium" + android:layout_toRightOf="@+id/image_featured" + android:visibility="gone" + tools:visibility="visible"> - - - + + + + - + android:layout_centerVertical="true" + android:layout_toLeftOf="@+id/related_post_follow_button" + android:layout_toRightOf="@+id/image_avatar" + android:orientation="vertical"> - + + + + + + + + + + \ No newline at end of file diff --git a/WordPress/src/main/res/layout/reader_related_post_divider.xml b/WordPress/src/main/res/layout/reader_related_post_divider.xml deleted file mode 100644 index 488260748d8a..000000000000 --- a/WordPress/src/main/res/layout/reader_related_post_divider.xml +++ /dev/null @@ -1,5 +0,0 @@ - - \ No newline at end of file diff --git a/WordPress/src/main/res/layout/reader_related_posts_view.xml b/WordPress/src/main/res/layout/reader_related_posts_view.xml new file mode 100644 index 000000000000..4bebb9a42474 --- /dev/null +++ b/WordPress/src/main/res/layout/reader_related_posts_view.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/WordPress/src/main/res/layout/reader_tag_strip.xml b/WordPress/src/main/res/layout/reader_tag_strip.xml new file mode 100644 index 000000000000..bc3ab11ef6a9 --- /dev/null +++ b/WordPress/src/main/res/layout/reader_tag_strip.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/WordPress/src/main/res/layout/reader_tag_strip_label.xml b/WordPress/src/main/res/layout/reader_tag_strip_label.xml new file mode 100644 index 000000000000..d703aefdb4f9 --- /dev/null +++ b/WordPress/src/main/res/layout/reader_tag_strip_label.xml @@ -0,0 +1,14 @@ + + + \ No newline at end of file diff --git a/WordPress/src/main/res/values/attrs.xml b/WordPress/src/main/res/values/attrs.xml index 6aa3afb9f5be..144082aa5bf3 100644 --- a/WordPress/src/main/res/values/attrs.xml +++ b/WordPress/src/main/res/values/attrs.xml @@ -64,6 +64,13 @@ + + + + + diff --git a/WordPress/src/main/res/values/colors.xml b/WordPress/src/main/res/values/colors.xml index e1d10b54d85c..432629423e93 100644 --- a/WordPress/src/main/res/values/colors.xml +++ b/WordPress/src/main/res/values/colors.xml @@ -111,6 +111,7 @@ @color/grey_lighten_30 @color/grey_dark + @color/blue_medium @color/alert_green @color/blue_medium #f8f8f8 diff --git a/WordPress/src/main/res/values/dimens.xml b/WordPress/src/main/res/values/dimens.xml index 139dd8db73aa..30ff799bf7a7 100644 --- a/WordPress/src/main/res/values/dimens.xml +++ b/WordPress/src/main/res/values/dimens.xml @@ -83,8 +83,9 @@ 96dp @dimen/reader_detail_margin_normal - @dimen/margin_extra_large - 48dp + 48dp + 56dp + 64dp 48dp @@ -111,8 +112,9 @@ 40dp 24dp - 16dp 48dp + 16dp + 24dp 48dp @@ -121,6 +123,8 @@ 340dp @dimen/reader_featured_image_height_default + 72dp + 24dp diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 62a29167f4a0..83077e1ab9b1 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1145,7 +1145,8 @@ Load more posts Search all public WordPress.com blogs Searching… - Related Reading + More in %s + More on WordPress.com View Gallery 1 image %d images diff --git a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java index 2a796f3eea3e..d5089c14723b 100644 --- a/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java +++ b/libs/utils/WordPressUtils/src/main/java/org/wordpress/android/util/DateTimeUtils.java @@ -44,19 +44,19 @@ public static String javaDateToTimeSpan(final Date date, Context context) { // less than an hour (ex: 12m) long minutesSince = secondsSince / 60; if (minutesSince < 60) { - return Long.toString(minutesSince) + "m"; + return Long.toString(minutesSince) + "m " + context.getString(R.string.timespan_ago); } // less than a day (ex: 17h) long hoursSince = minutesSince / 60; if (hoursSince < 24) { - return Long.toString(hoursSince) + "h"; + return Long.toString(hoursSince) + "h " + context.getString(R.string.timespan_ago); } // less than a week (ex: 5d) long daysSince = hoursSince / 24; if (daysSince < 7) { - return Long.toString(daysSince) + "d"; + return Long.toString(daysSince) + "d " + context.getString(R.string.timespan_ago); } // less than a year old, so return day/month without year (ex: Jan 30) diff --git a/libs/utils/WordPressUtils/src/main/res/values/strings.xml b/libs/utils/WordPressUtils/src/main/res/values/strings.xml index 34d25dada509..b24207e6e1f7 100644 --- a/libs/utils/WordPressUtils/src/main/res/values/strings.xml +++ b/libs/utils/WordPressUtils/src/main/res/values/strings.xml @@ -2,4 +2,5 @@ There is no network available Now + ago