| package org.wordpress.android.ui.reader.adapters; |
| |
| import android.content.Context; |
| import android.os.AsyncTask; |
| import android.support.v7.widget.CardView; |
| import android.support.v7.widget.RecyclerView; |
| import android.text.TextUtils; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.ImageView; |
| import android.widget.LinearLayout; |
| 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.ReaderPostTable; |
| import org.wordpress.android.models.ReaderPost; |
| import org.wordpress.android.models.ReaderPostDiscoverData; |
| import org.wordpress.android.models.ReaderPostList; |
| import org.wordpress.android.models.ReaderTag; |
| import org.wordpress.android.ui.reader.ReaderActivityLauncher; |
| import org.wordpress.android.ui.reader.ReaderAnim; |
| import org.wordpress.android.ui.reader.ReaderConstants; |
| import org.wordpress.android.ui.reader.ReaderInterfaces; |
| import org.wordpress.android.ui.reader.ReaderTypes; |
| 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.utils.ReaderUtils; |
| import org.wordpress.android.ui.reader.utils.ReaderXPostUtils; |
| import org.wordpress.android.ui.reader.views.ReaderFollowButton; |
| import org.wordpress.android.ui.reader.views.ReaderGapMarkerView; |
| import org.wordpress.android.ui.reader.views.ReaderIconCountView; |
| import org.wordpress.android.ui.reader.views.ReaderSiteHeaderView; |
| import org.wordpress.android.ui.reader.views.ReaderTagHeaderView; |
| import org.wordpress.android.ui.reader.views.ReaderThumbnailStrip; |
| import org.wordpress.android.util.AnalyticsUtils; |
| import org.wordpress.android.util.AppLog; |
| 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.ToastUtils; |
| import org.wordpress.android.util.UrlUtils; |
| import org.wordpress.android.widgets.WPNetworkImageView; |
| |
| import java.util.HashSet; |
| |
| public class ReaderPostAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { |
| private ReaderTag mCurrentTag; |
| private long mCurrentBlogId; |
| private long mCurrentFeedId; |
| private int mGapMarkerPosition = -1; |
| |
| private final int mPhotonWidth; |
| private final int mPhotonHeight; |
| private final int mAvatarSzMedium; |
| private final int mAvatarSzSmall; |
| private final int mAvatarSzTiny; |
| private final int mMarginLarge; |
| |
| private boolean mCanRequestMorePosts; |
| private final boolean mIsLoggedOutReader; |
| |
| private final ReaderTypes.ReaderPostListType mPostListType; |
| private final ReaderPostList mPosts = new ReaderPostList(); |
| private final HashSet<String> mRenderedIds = new HashSet<>(); |
| |
| private ReaderInterfaces.OnPostSelectedListener mPostSelectedListener; |
| private ReaderInterfaces.OnTagSelectedListener mOnTagSelectedListener; |
| private ReaderInterfaces.OnPostPopupListener mOnPostPopupListener; |
| private ReaderInterfaces.DataLoadedListener mDataLoadedListener; |
| private ReaderActions.DataRequestedListener mDataRequestedListener; |
| private ReaderSiteHeaderView.OnBlogInfoLoadedListener mBlogInfoLoadedListener; |
| |
| // the large "tbl_posts.text" column is unused here, so skip it when querying |
| private static final boolean EXCLUDE_TEXT_COLUMN = true; |
| private static final int MAX_ROWS = ReaderConstants.READER_MAX_POSTS_TO_DISPLAY; |
| |
| private static final int VIEW_TYPE_POST = 0; |
| private static final int VIEW_TYPE_XPOST = 1; |
| private static final int VIEW_TYPE_SITE_HEADER = 2; |
| private static final int VIEW_TYPE_TAG_HEADER = 3; |
| private static final int VIEW_TYPE_GAP_MARKER = 4; |
| |
| private static final long ITEM_ID_CUSTOM_VIEW = -1L; |
| |
| /* |
| * cross-post |
| */ |
| class ReaderXPostViewHolder extends RecyclerView.ViewHolder { |
| private final CardView cardView; |
| private final WPNetworkImageView imgAvatar; |
| private final WPNetworkImageView imgBlavatar; |
| private final TextView txtTitle; |
| private final TextView txtSubtitle; |
| |
| public ReaderXPostViewHolder(View itemView) { |
| super(itemView); |
| cardView = (CardView) itemView.findViewById(R.id.card_view); |
| imgAvatar = (WPNetworkImageView) itemView.findViewById(R.id.image_avatar); |
| imgBlavatar = (WPNetworkImageView) itemView.findViewById(R.id.image_blavatar); |
| txtTitle = (TextView) itemView.findViewById(R.id.text_title); |
| txtSubtitle = (TextView) itemView.findViewById(R.id.text_subtitle); |
| } |
| } |
| |
| /* |
| * full post |
| */ |
| class ReaderPostViewHolder extends RecyclerView.ViewHolder { |
| private final CardView cardView; |
| |
| private final TextView txtTitle; |
| private final TextView txtText; |
| private final TextView txtBlogName; |
| private final TextView txtDomain; |
| private final TextView txtDateline; |
| private final TextView txtTag; |
| |
| private final ReaderIconCountView commentCount; |
| private final ReaderIconCountView likeCount; |
| |
| private final ImageView imgMore; |
| |
| private final WPNetworkImageView imgFeatured; |
| private final WPNetworkImageView imgAvatar; |
| private final WPNetworkImageView imgBlavatar; |
| |
| private final ViewGroup layoutPostHeader; |
| private final ReaderFollowButton followButton; |
| |
| private final ViewGroup layoutDiscover; |
| private final WPNetworkImageView imgDiscoverAvatar; |
| private final TextView txtDiscover; |
| |
| private final ReaderThumbnailStrip thumbnailStrip; |
| |
| public ReaderPostViewHolder(View itemView) { |
| super(itemView); |
| |
| cardView = (CardView) itemView.findViewById(R.id.card_view); |
| |
| txtTitle = (TextView) itemView.findViewById(R.id.text_title); |
| txtText = (TextView) itemView.findViewById(R.id.text_excerpt); |
| txtBlogName = (TextView) itemView.findViewById(R.id.text_blog_name); |
| txtDomain = (TextView) itemView.findViewById(R.id.text_domain); |
| txtDateline = (TextView) itemView.findViewById(R.id.text_dateline); |
| txtTag = (TextView) itemView.findViewById(R.id.text_tag); |
| |
| commentCount = (ReaderIconCountView) itemView.findViewById(R.id.count_comments); |
| likeCount = (ReaderIconCountView) itemView.findViewById(R.id.count_likes); |
| |
| imgFeatured = (WPNetworkImageView) itemView.findViewById(R.id.image_featured); |
| imgBlavatar = (WPNetworkImageView) itemView.findViewById(R.id.image_blavatar); |
| imgAvatar = (WPNetworkImageView) itemView.findViewById(R.id.image_avatar); |
| imgMore = (ImageView) itemView.findViewById(R.id.image_more); |
| |
| layoutDiscover = (ViewGroup) itemView.findViewById(R.id.layout_discover); |
| imgDiscoverAvatar = (WPNetworkImageView) layoutDiscover.findViewById(R.id.image_discover_avatar); |
| txtDiscover = (TextView) layoutDiscover.findViewById(R.id.text_discover); |
| |
| thumbnailStrip = (ReaderThumbnailStrip) itemView.findViewById(R.id.thumbnail_strip); |
| |
| layoutPostHeader = (ViewGroup) itemView.findViewById(R.id.layout_post_header); |
| followButton = (ReaderFollowButton) layoutPostHeader.findViewById(R.id.follow_button); |
| |
| // post header isn't shown when there's a site header, so add a bit more |
| // padding above the title |
| if (hasSiteHeader()) { |
| int extraPadding = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.margin_medium); |
| txtTitle.setPadding( |
| txtTitle.getPaddingLeft(), |
| txtTitle.getPaddingTop() + extraPadding, |
| txtTitle.getPaddingRight(), |
| txtTitle.getPaddingBottom()); |
| } |
| |
| ReaderUtils.setBackgroundToRoundRipple(imgMore); |
| } |
| } |
| |
| class SiteHeaderViewHolder extends RecyclerView.ViewHolder { |
| private final ReaderSiteHeaderView mSiteHeaderView; |
| public SiteHeaderViewHolder(View itemView) { |
| super(itemView); |
| mSiteHeaderView = (ReaderSiteHeaderView) itemView; |
| } |
| } |
| |
| class TagHeaderViewHolder extends RecyclerView.ViewHolder { |
| private final ReaderTagHeaderView mTagHeaderView; |
| public TagHeaderViewHolder(View itemView) { |
| super(itemView); |
| mTagHeaderView = (ReaderTagHeaderView) itemView; |
| } |
| } |
| |
| class GapMarkerViewHolder extends RecyclerView.ViewHolder { |
| private final ReaderGapMarkerView mGapMarkerView; |
| public GapMarkerViewHolder(View itemView) { |
| super(itemView); |
| mGapMarkerView = (ReaderGapMarkerView) itemView; |
| } |
| } |
| |
| @Override |
| public int getItemViewType(int position) { |
| if (position == 0 && hasSiteHeader()) { |
| // first item is a ReaderSiteHeaderView |
| return VIEW_TYPE_SITE_HEADER; |
| } else if (position == 0 && hasTagHeader()) { |
| // first item is a ReaderTagHeaderView |
| return VIEW_TYPE_TAG_HEADER; |
| } else if (position == mGapMarkerPosition) { |
| return VIEW_TYPE_GAP_MARKER; |
| } else if (getItem(position).isXpost()) { |
| return VIEW_TYPE_XPOST; |
| } else { |
| return VIEW_TYPE_POST; |
| } |
| } |
| |
| @Override |
| public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
| Context context = parent.getContext(); |
| switch (viewType) { |
| case VIEW_TYPE_SITE_HEADER: |
| return new SiteHeaderViewHolder(new ReaderSiteHeaderView(context)); |
| |
| case VIEW_TYPE_TAG_HEADER: |
| return new TagHeaderViewHolder(new ReaderTagHeaderView(context)); |
| |
| case VIEW_TYPE_GAP_MARKER: |
| return new GapMarkerViewHolder(new ReaderGapMarkerView(context)); |
| |
| case VIEW_TYPE_XPOST: |
| View xpostView = LayoutInflater.from(context).inflate(R.layout.reader_cardview_xpost, parent, false); |
| return new ReaderXPostViewHolder(xpostView); |
| |
| default: |
| View postView = LayoutInflater.from(context).inflate(R.layout.reader_cardview_post, parent, false); |
| return new ReaderPostViewHolder(postView); |
| } |
| } |
| |
| @Override |
| public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { |
| if (holder instanceof ReaderPostViewHolder) { |
| renderPost(position, (ReaderPostViewHolder) holder); |
| } else if (holder instanceof ReaderXPostViewHolder) { |
| renderXPost(position, (ReaderXPostViewHolder) holder); |
| } else if (holder instanceof SiteHeaderViewHolder) { |
| SiteHeaderViewHolder siteHolder = (SiteHeaderViewHolder) holder; |
| siteHolder.mSiteHeaderView.setOnBlogInfoLoadedListener(mBlogInfoLoadedListener); |
| if (isDiscover()) { |
| siteHolder.mSiteHeaderView.loadBlogInfo(ReaderConstants.DISCOVER_SITE_ID, 0); |
| } else { |
| siteHolder.mSiteHeaderView.loadBlogInfo(mCurrentBlogId, mCurrentFeedId); |
| } |
| } else if (holder instanceof TagHeaderViewHolder) { |
| TagHeaderViewHolder tagHolder = (TagHeaderViewHolder) holder; |
| tagHolder.mTagHeaderView.setCurrentTag(mCurrentTag); |
| } else if (holder instanceof GapMarkerViewHolder) { |
| GapMarkerViewHolder gapHolder = (GapMarkerViewHolder) holder; |
| gapHolder.mGapMarkerView.setCurrentTag(mCurrentTag); |
| } |
| } |
| |
| private void renderXPost(int position, ReaderXPostViewHolder holder) { |
| final ReaderPost post = getItem(position); |
| |
| if (post.hasPostAvatar()) { |
| holder.imgAvatar.setImageUrl( |
| post.getPostAvatarForDisplay(mAvatarSzSmall), WPNetworkImageView.ImageType.AVATAR); |
| } else { |
| holder.imgAvatar.showDefaultGravatarImage(); |
| } |
| |
| if (post.hasBlogUrl()) { |
| holder.imgBlavatar.setImageUrl( |
| post.getPostBlavatarForDisplay(mAvatarSzMedium), WPNetworkImageView.ImageType.BLAVATAR); |
| } else { |
| holder.imgBlavatar.showDefaultBlavatarImage(); |
| } |
| |
| holder.txtTitle.setText(ReaderXPostUtils.getXPostTitle(post)); |
| holder.txtSubtitle.setText(ReaderXPostUtils.getXPostSubtitleHtml(post)); |
| |
| holder.cardView.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mPostSelectedListener != null) { |
| mPostSelectedListener.onPostSelected(post); |
| } |
| } |
| }); |
| |
| checkLoadMore(position); |
| } |
| |
| private void renderPost(int position, ReaderPostViewHolder holder) { |
| final ReaderPost post = getItem(position); |
| ReaderTypes.ReaderPostListType postListType = getPostListType(); |
| |
| holder.txtTitle.setText(post.getTitle()); |
| |
| String timestamp = DateTimeUtils.javaDateToTimeSpan(post.getDisplayDate(), WordPress.getContext()); |
| if (post.hasAuthorName()) { |
| holder.txtDateline.setText(post.getAuthorName() + ReaderConstants.UNICODE_BULLET_WITH_SPACE + timestamp); |
| } else if (post.hasBlogName()) { |
| holder.txtDateline.setText(post.getBlogName() + ReaderConstants.UNICODE_BULLET_WITH_SPACE + timestamp); |
| } else { |
| holder.txtDateline.setText(timestamp); |
| } |
| |
| if (post.hasPostAvatar()) { |
| holder.imgAvatar.setImageUrl( |
| post.getPostAvatarForDisplay(mAvatarSzTiny), WPNetworkImageView.ImageType.AVATAR); |
| } else { |
| holder.imgAvatar.showDefaultGravatarImage(); |
| } |
| |
| // post header isn't show when there's a site header |
| if (hasSiteHeader()) { |
| holder.layoutPostHeader.setVisibility(View.GONE); |
| } else { |
| holder.layoutPostHeader.setVisibility(View.VISIBLE); |
| // show blog preview when post header is tapped |
| holder.layoutPostHeader.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| ReaderActivityLauncher.showReaderBlogPreview(view.getContext(), post); |
| } |
| }); |
| } |
| |
| if (post.hasBlogUrl()) { |
| String imageUrl = GravatarUtils.blavatarFromUrl(post.getBlogUrl(), mAvatarSzMedium); |
| holder.imgBlavatar.setImageUrl(imageUrl, WPNetworkImageView.ImageType.BLAVATAR); |
| holder.txtDomain.setText(UrlUtils.getHost(post.getBlogUrl())); |
| } else { |
| holder.imgBlavatar.showDefaultBlavatarImage(); |
| holder.txtDomain.setText(null); |
| } |
| if (post.hasBlogName()) { |
| holder.txtBlogName.setText(post.getBlogName()); |
| } else if (post.hasAuthorName()) { |
| holder.txtBlogName.setText(post.getAuthorName()); |
| } else { |
| holder.txtBlogName.setText(null); |
| } |
| |
| if (post.hasExcerpt()) { |
| holder.txtText.setVisibility(View.VISIBLE); |
| holder.txtText.setText(post.getExcerpt()); |
| } else { |
| holder.txtText.setVisibility(View.GONE); |
| } |
| |
| final int titleMargin; |
| if (post.hasFeaturedImage()) { |
| final String imageUrl = post.getFeaturedImageForDisplay(mPhotonWidth, mPhotonHeight); |
| holder.imgFeatured.setImageUrl(imageUrl, WPNetworkImageView.ImageType.PHOTO); |
| holder.imgFeatured.setVisibility(View.VISIBLE); |
| titleMargin = mMarginLarge; |
| } else if (post.hasFeaturedVideo() && WPNetworkImageView.canShowVideoThumbnail(post.getFeaturedVideo())) { |
| holder.imgFeatured.setVideoUrl(post.postId, post.getFeaturedVideo()); |
| holder.imgFeatured.setVisibility(View.VISIBLE); |
| titleMargin = mMarginLarge; |
| } else { |
| holder.imgFeatured.setVisibility(View.GONE); |
| titleMargin = (holder.layoutPostHeader.getVisibility() == View.VISIBLE ? 0 : mMarginLarge); |
| } |
| |
| // set the top margin of the title based on whether there's a featured image and post header |
| LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) holder.txtTitle.getLayoutParams(); |
| params.topMargin = titleMargin; |
| |
| // show the best tag for this post |
| final String tagToDisplay = (mCurrentTag != null ? post.getTagForDisplay(mCurrentTag.getTagSlug()) : null); |
| if (!TextUtils.isEmpty(tagToDisplay)) { |
| holder.txtTag.setText(ReaderUtils.makeHashTag(tagToDisplay)); |
| holder.txtTag.setVisibility(View.VISIBLE); |
| holder.txtTag.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mOnTagSelectedListener != null) { |
| mOnTagSelectedListener.onTagSelected(tagToDisplay); |
| } |
| } |
| }); |
| } else { |
| holder.txtTag.setVisibility(View.GONE); |
| holder.txtTag.setOnClickListener(null); |
| } |
| |
| showLikes(holder, post); |
| showComments(holder, post); |
| |
| // more menu only shows for followed tags |
| if (!mIsLoggedOutReader && postListType == ReaderTypes.ReaderPostListType.TAG_FOLLOWED) { |
| holder.imgMore.setVisibility(View.VISIBLE); |
| holder.imgMore.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| if (mOnPostPopupListener != null) { |
| mOnPostPopupListener.onShowPostPopup(view, post); |
| } |
| } |
| }); |
| } else { |
| holder.imgMore.setVisibility(View.GONE); |
| holder.imgMore.setOnClickListener(null); |
| } |
| |
| // follow button doesn't show for "Followed Sites" or when there's a site header (Discover, site preview) |
| boolean showFollowButton = !hasSiteHeader() |
| && !mIsLoggedOutReader |
| && !isFollowedSites(); |
| if (showFollowButton) { |
| holder.followButton.setIsFollowed(post.isFollowedByCurrentUser); |
| holder.followButton.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| toggleFollow(view.getContext(), view, post); |
| } |
| }); |
| holder.followButton.setVisibility(View.VISIBLE); |
| } else { |
| holder.followButton.setVisibility(View.GONE); |
| } |
| |
| // attribution section for discover posts |
| if (post.isDiscoverPost()) { |
| showDiscoverData(holder, post); |
| } else { |
| holder.layoutDiscover.setVisibility(View.GONE); |
| } |
| |
| // if this post has attachments or contains a gallery, scan it for images and show a |
| // thumbnail strip of them - note that the thumbnail strip will take care of making |
| // itself visible |
| if (post.hasAttachments() || post.isGallery()) { |
| holder.thumbnailStrip.loadThumbnails(post.blogId, post.postId, post.isPrivate); |
| } else { |
| holder.thumbnailStrip.setVisibility(View.GONE); |
| } |
| |
| holder.cardView.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mPostSelectedListener != null) { |
| mPostSelectedListener.onPostSelected(post); |
| } |
| } |
| }); |
| |
| checkLoadMore(position); |
| |
| // if we haven't already rendered this post and it has a "railcar" attached to it, add it |
| // to the rendered list and record the TrainTracks render event |
| if (post.hasRailcar() && !mRenderedIds.contains(post.getPseudoId())) { |
| mRenderedIds.add(post.getPseudoId()); |
| AnalyticsUtils.trackRailcarRender(post.getRailcarJson()); |
| } |
| } |
| |
| /* |
| * if we're nearing the end of the posts, fire request to load more |
| */ |
| private void checkLoadMore(int position) { |
| if (mCanRequestMorePosts |
| && mDataRequestedListener != null |
| && (position >= getItemCount() - 1)) { |
| mDataRequestedListener.onRequestData(); |
| } |
| } |
| |
| private void showDiscoverData(final ReaderPostViewHolder postHolder, |
| final ReaderPost post) { |
| final ReaderPostDiscoverData discoverData = post.getDiscoverData(); |
| if (discoverData == null) { |
| postHolder.layoutDiscover.setVisibility(View.GONE); |
| return; |
| } |
| |
| postHolder.layoutDiscover.setVisibility(View.VISIBLE); |
| postHolder.txtDiscover.setText(discoverData.getAttributionHtml()); |
| |
| switch (discoverData.getDiscoverType()) { |
| case EDITOR_PICK: |
| if (discoverData.hasAvatarUrl()) { |
| postHolder.imgDiscoverAvatar.setImageUrl(GravatarUtils.fixGravatarUrl(discoverData.getAvatarUrl(), mAvatarSzSmall), WPNetworkImageView.ImageType.AVATAR); |
| } else { |
| postHolder.imgDiscoverAvatar.showDefaultGravatarImage(); |
| } |
| // tapping an editor pick opens the source post, which is handled by the existing |
| // post selection handler |
| postHolder.layoutDiscover.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (mPostSelectedListener != null) { |
| mPostSelectedListener.onPostSelected(post); |
| } |
| } |
| }); |
| break; |
| |
| case SITE_PICK: |
| if (discoverData.hasAvatarUrl()) { |
| postHolder.imgDiscoverAvatar.setImageUrl( |
| GravatarUtils.fixGravatarUrl(discoverData.getAvatarUrl(), mAvatarSzSmall), WPNetworkImageView.ImageType.BLAVATAR); |
| } else { |
| postHolder.imgDiscoverAvatar.showDefaultBlavatarImage(); |
| } |
| // site picks show "Visit [BlogName]" link - tapping opens the blog preview if |
| // we have the blogId, if not show blog in internal webView |
| postHolder.layoutDiscover.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| if (discoverData.getBlogId() != 0) { |
| ReaderActivityLauncher.showReaderBlogPreview( |
| v.getContext(), |
| discoverData.getBlogId()); |
| } else if (discoverData.hasBlogUrl()) { |
| ReaderActivityLauncher.openUrl(v.getContext(), discoverData.getBlogUrl()); |
| } |
| } |
| }); |
| break; |
| |
| default: |
| // something else, so hide discover section |
| postHolder.layoutDiscover.setVisibility(View.GONE); |
| break; |
| } |
| } |
| |
| // ******************************************************************************************** |
| |
| public ReaderPostAdapter(Context context, ReaderTypes.ReaderPostListType postListType) { |
| super(); |
| |
| mPostListType = postListType; |
| mAvatarSzMedium = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_medium); |
| mAvatarSzSmall = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_small); |
| mAvatarSzTiny = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_tiny); |
| mMarginLarge = context.getResources().getDimensionPixelSize(R.dimen.margin_large); |
| mIsLoggedOutReader = ReaderUtils.isLoggedOutReader(); |
| |
| int displayWidth = DisplayUtils.getDisplayPixelWidth(context); |
| int cardMargin = context.getResources().getDimensionPixelSize(R.dimen.reader_card_margin); |
| mPhotonWidth = displayWidth - (cardMargin * 2); |
| mPhotonHeight = context.getResources().getDimensionPixelSize(R.dimen.reader_featured_image_height); |
| |
| setHasStableIds(true); |
| } |
| |
| private boolean hasCustomFirstItem() { |
| return hasSiteHeader() || hasTagHeader(); |
| } |
| |
| private boolean hasSiteHeader() { |
| return isDiscover() || getPostListType() == ReaderTypes.ReaderPostListType.BLOG_PREVIEW; |
| } |
| |
| private boolean hasTagHeader() { |
| return getPostListType() == ReaderTypes.ReaderPostListType.TAG_PREVIEW; |
| } |
| |
| private boolean isDiscover() { |
| return mCurrentTag != null && mCurrentTag.isDiscover(); |
| } |
| |
| private boolean isFollowedSites() { |
| return mCurrentTag != null && mCurrentTag.isFollowedSites(); |
| } |
| |
| public void setOnPostSelectedListener(ReaderInterfaces.OnPostSelectedListener listener) { |
| mPostSelectedListener = listener; |
| } |
| |
| public void setOnDataLoadedListener(ReaderInterfaces.DataLoadedListener listener) { |
| mDataLoadedListener = listener; |
| } |
| |
| public void setOnDataRequestedListener(ReaderActions.DataRequestedListener listener) { |
| mDataRequestedListener = listener; |
| } |
| |
| public void setOnPostPopupListener(ReaderInterfaces.OnPostPopupListener onPostPopupListener) { |
| mOnPostPopupListener = onPostPopupListener; |
| } |
| |
| public void setOnBlogInfoLoadedListener(ReaderSiteHeaderView.OnBlogInfoLoadedListener listener) { |
| mBlogInfoLoadedListener = listener; |
| } |
| |
| /* |
| * called when user clicks a tag |
| */ |
| public void setOnTagSelectedListener(ReaderInterfaces.OnTagSelectedListener listener) { |
| mOnTagSelectedListener = listener; |
| } |
| |
| private ReaderTypes.ReaderPostListType getPostListType() { |
| return (mPostListType != null ? mPostListType : ReaderTypes.DEFAULT_POST_LIST_TYPE); |
| } |
| |
| // used when the viewing tagged posts |
| public void setCurrentTag(ReaderTag tag) { |
| if (!ReaderTag.isSameTag(tag, mCurrentTag)) { |
| mCurrentTag = tag; |
| mRenderedIds.clear(); |
| reload(); |
| } |
| } |
| |
| public boolean isCurrentTag(ReaderTag tag) { |
| return ReaderTag.isSameTag(tag, mCurrentTag); |
| } |
| |
| // used when the list type is ReaderPostListType.BLOG_PREVIEW |
| public void setCurrentBlogAndFeed(long blogId, long feedId) { |
| if (blogId != mCurrentBlogId || feedId != mCurrentFeedId) { |
| mCurrentBlogId = blogId; |
| mCurrentFeedId = feedId; |
| mRenderedIds.clear(); |
| reload(); |
| } |
| } |
| |
| public void clear() { |
| if (!mPosts.isEmpty()) { |
| mPosts.clear(); |
| notifyDataSetChanged(); |
| } |
| } |
| |
| public void refresh() { |
| loadPosts(); |
| } |
| |
| /* |
| * same as refresh() above but first clears the existing posts |
| */ |
| public void reload() { |
| clear(); |
| loadPosts(); |
| } |
| |
| public void removePostsInBlog(long blogId) { |
| int numRemoved = 0; |
| ReaderPostList postsInBlog = mPosts.getPostsInBlog(blogId); |
| for (ReaderPost post : postsInBlog) { |
| int index = mPosts.indexOfPost(post); |
| if (index > -1) { |
| numRemoved++; |
| mPosts.remove(index); |
| } |
| } |
| if (numRemoved > 0) { |
| notifyDataSetChanged(); |
| } |
| } |
| |
| private void loadPosts() { |
| if (mIsTaskRunning) { |
| AppLog.w(AppLog.T.READER, "reader posts task already running"); |
| return; |
| } |
| new LoadPostsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |
| } |
| |
| private ReaderPost getItem(int position) { |
| if (position == 0 && hasCustomFirstItem()) { |
| return null; |
| } |
| if (position == mGapMarkerPosition) { |
| return null; |
| } |
| |
| int arrayPos = hasCustomFirstItem() ? position - 1 : position; |
| |
| if (mGapMarkerPosition > -1 && position > mGapMarkerPosition) { |
| arrayPos--; |
| } |
| |
| return mPosts.get(arrayPos); |
| } |
| |
| @Override |
| public int getItemCount() { |
| if (hasCustomFirstItem()) { |
| return mPosts.size() + 1; |
| } |
| return mPosts.size(); |
| } |
| |
| public boolean isEmpty() { |
| return (mPosts == null || mPosts.size() == 0); |
| } |
| |
| @Override |
| public long getItemId(int position) { |
| if (getItemViewType(position) == VIEW_TYPE_POST) { |
| return getItem(position).getStableId(); |
| } else { |
| return ITEM_ID_CUSTOM_VIEW; |
| } |
| } |
| |
| private void showLikes(final ReaderPostViewHolder holder, final ReaderPost post) { |
| boolean canShowLikes; |
| if (post.isDiscoverPost()) { |
| canShowLikes = false; |
| } else if (mIsLoggedOutReader) { |
| canShowLikes = post.numLikes > 0; |
| } else { |
| canShowLikes = post.canLikePost(); |
| } |
| |
| if (canShowLikes) { |
| holder.likeCount.setCount(post.numLikes); |
| holder.likeCount.setSelected(post.isLikedByCurrentUser); |
| holder.likeCount.setVisibility(View.VISIBLE); |
| // can't like when logged out |
| if (!mIsLoggedOutReader) { |
| holder.likeCount.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| toggleLike(v.getContext(), holder, post); |
| } |
| }); |
| } |
| } else { |
| holder.likeCount.setVisibility(View.GONE); |
| holder.likeCount.setOnClickListener(null); |
| } |
| } |
| |
| private void showComments(final ReaderPostViewHolder holder, final ReaderPost post) { |
| boolean canShowComments; |
| if (post.isDiscoverPost()) { |
| canShowComments = false; |
| } else if (mIsLoggedOutReader) { |
| canShowComments = post.numReplies > 0; |
| } else { |
| canShowComments = post.isWP() && !post.isJetpack && (post.isCommentsOpen || post.numReplies > 0); |
| } |
| |
| if (canShowComments) { |
| holder.commentCount.setCount(post.numReplies); |
| holder.commentCount.setVisibility(View.VISIBLE); |
| holder.commentCount.setOnClickListener(new View.OnClickListener() { |
| @Override |
| public void onClick(View v) { |
| ReaderActivityLauncher.showReaderComments(v.getContext(), post.blogId, post.postId); |
| } |
| }); |
| } else { |
| holder.commentCount.setVisibility(View.GONE); |
| holder.commentCount.setOnClickListener(null); |
| } |
| } |
| |
| /* |
| * triggered when user taps the like button (textView) |
| */ |
| private void toggleLike(Context context, ReaderPostViewHolder holder, ReaderPost post) { |
| if (post == null || !NetworkUtils.checkConnection(context)) { |
| return; |
| } |
| |
| boolean isCurrentlyLiked = ReaderPostTable.isPostLikedByCurrentUser(post); |
| boolean isAskingToLike = !isCurrentlyLiked; |
| ReaderAnim.animateLikeButton(holder.likeCount.getImageView(), isAskingToLike); |
| |
| if (!ReaderPostActions.performLikeAction(post, isAskingToLike)) { |
| ToastUtils.showToast(context, R.string.reader_toast_err_generic); |
| return; |
| } |
| |
| if (isAskingToLike) { |
| AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_LIKED, post); |
| // Consider a like to be enough to push a page view - solves a long-standing question |
| // from folks who ask 'why do I have more likes than page views?'. |
| ReaderPostActions.bumpPageViewForPost(post); |
| } else { |
| AnalyticsUtils.trackWithReaderPostDetails(AnalyticsTracker.Stat.READER_ARTICLE_LIKED, post); |
| } |
| |
| // update post in array and on screen |
| int position = mPosts.indexOfPost(post); |
| ReaderPost updatedPost = ReaderPostTable.getPost(post.blogId, post.postId, true); |
| if (updatedPost != null && position > -1) { |
| mPosts.set(position, updatedPost); |
| showLikes(holder, updatedPost); |
| } |
| } |
| |
| /* |
| * triggered when user taps the follow button on a post |
| */ |
| private void toggleFollow(final Context context, final View followButton, final ReaderPost post) { |
| if (post == null || !NetworkUtils.checkConnection(context)) { |
| return; |
| } |
| |
| boolean isCurrentlyFollowed = ReaderPostTable.isPostFollowed(post); |
| final boolean isAskingToFollow = !isCurrentlyFollowed; |
| |
| ReaderActions.ActionListener actionListener = new ReaderActions.ActionListener() { |
| @Override |
| public void onActionResult(boolean succeeded) { |
| followButton.setEnabled(true); |
| if (!succeeded) { |
| int resId = (isAskingToFollow ? R.string.reader_toast_err_follow_blog : R.string.reader_toast_err_unfollow_blog); |
| ToastUtils.showToast(context, resId); |
| setFollowStatusForBlog(post.blogId, !isAskingToFollow); |
| } |
| } |
| }; |
| |
| if (!ReaderBlogActions.followBlogForPost(post, isAskingToFollow, actionListener)) { |
| ToastUtils.showToast(context, R.string.reader_toast_err_generic); |
| return; |
| } |
| |
| followButton.setEnabled(false); |
| setFollowStatusForBlog(post.blogId, isAskingToFollow); |
| } |
| |
| public void setFollowStatusForBlog(long blogId, boolean isFollowing) { |
| ReaderPost post; |
| for (int i = 0; i < mPosts.size(); i++) { |
| post = mPosts.get(i); |
| if (post.blogId == blogId && post.isFollowedByCurrentUser != isFollowing) { |
| post.isFollowedByCurrentUser = isFollowing; |
| mPosts.set(i, post); |
| notifyItemChanged(i); |
| } |
| } |
| } |
| |
| public void removeGapMarker() { |
| if (mGapMarkerPosition == -1) return; |
| |
| int position = mGapMarkerPosition; |
| mGapMarkerPosition = -1; |
| if (position < getItemCount()) { |
| notifyItemRemoved(position); |
| } |
| } |
| |
| /* |
| * AsyncTask to load posts in the current tag |
| */ |
| private boolean mIsTaskRunning = false; |
| |
| private class LoadPostsTask extends AsyncTask<Void, Void, Boolean> { |
| ReaderPostList allPosts; |
| |
| @Override |
| protected void onPreExecute() { |
| mIsTaskRunning = true; |
| } |
| |
| @Override |
| protected void onCancelled() { |
| mIsTaskRunning = false; |
| } |
| |
| @Override |
| protected Boolean doInBackground(Void... params) { |
| int numExisting; |
| switch (getPostListType()) { |
| case TAG_PREVIEW: |
| case TAG_FOLLOWED: |
| case SEARCH_RESULTS: |
| allPosts = ReaderPostTable.getPostsWithTag(mCurrentTag, MAX_ROWS, EXCLUDE_TEXT_COLUMN); |
| numExisting = ReaderPostTable.getNumPostsWithTag(mCurrentTag); |
| break; |
| case BLOG_PREVIEW: |
| if (mCurrentFeedId != 0) { |
| allPosts = ReaderPostTable.getPostsInFeed(mCurrentFeedId, MAX_ROWS, EXCLUDE_TEXT_COLUMN); |
| numExisting = ReaderPostTable.getNumPostsInFeed(mCurrentFeedId); |
| } else { |
| allPosts = ReaderPostTable.getPostsInBlog(mCurrentBlogId, MAX_ROWS, EXCLUDE_TEXT_COLUMN); |
| numExisting = ReaderPostTable.getNumPostsInBlog(mCurrentBlogId); |
| } |
| break; |
| default: |
| return false; |
| } |
| |
| if (mPosts.isSameList(allPosts)) { |
| return false; |
| } |
| |
| // if we're not already displaying the max # posts, enable requesting more when |
| // the user scrolls to the end of the list |
| mCanRequestMorePosts = (numExisting < ReaderConstants.READER_MAX_POSTS_TO_DISPLAY); |
| |
| // determine whether a gap marker exists - only applies to tagged posts |
| mGapMarkerPosition = getGapMarkerPosition(); |
| |
| return true; |
| } |
| |
| private int getGapMarkerPosition() { |
| if (!getPostListType().isTagType()) { |
| return -1; |
| } |
| |
| ReaderBlogIdPostId gapMarkerIds = ReaderPostTable.getGapMarkerIdsForTag(mCurrentTag); |
| if (gapMarkerIds == null) { |
| return -1; |
| } |
| |
| // find the position of the gap marker post |
| int gapPosition = allPosts.indexOfIds(gapMarkerIds); |
| if (gapPosition > -1) { |
| // increment it because we want the gap marker to appear *below* this post |
| gapPosition++; |
| // increment it again if there's a custom first item |
| if (hasCustomFirstItem()) { |
| gapPosition++; |
| } |
| // remove the gap marker if it's on the last post (edge case but |
| // it can happen following a purge) |
| if (gapPosition >= allPosts.size() - 1) { |
| gapPosition = -1; |
| AppLog.w(AppLog.T.READER, "gap marker at/after last post, removed"); |
| ReaderPostTable.removeGapMarkerForTag(mCurrentTag); |
| } else { |
| AppLog.d(AppLog.T.READER, "gap marker at position " + gapPosition); |
| } |
| } |
| return gapPosition; |
| } |
| |
| @Override |
| protected void onPostExecute(Boolean result) { |
| if (result) { |
| mPosts.clear(); |
| mPosts.addAll(allPosts); |
| notifyDataSetChanged(); |
| } |
| |
| if (mDataLoadedListener != null) { |
| mDataLoadedListener.onDataLoaded(isEmpty()); |
| } |
| |
| mIsTaskRunning = false; |
| } |
| } |
| } |