aboutsummaryrefslogtreecommitdiff
path: root/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters
diff options
context:
space:
mode:
authorChris Warrington <cmw@google.com>2016-10-18 12:29:21 +0100
committerChris Warrington <cmw@google.com>2016-10-18 12:34:18 +0100
commite3780081075c01aa1dff6d1f373cb43192b33e68 (patch)
treefb734615933a39f3d009210dc0d1457160479b35 /WordPress/src/main/java/org/wordpress/android/ui/reader/adapters
parent7e05eb7e57827eddc885570bc00aed8a50320dbf (diff)
parent025b8b226c8d8edba2b309ca878572f40512eca7 (diff)
downloadgradle-perf-android-medium-main.tar.gz
Change-Id: I63f5e16d09297c48432192761b840310935eb903
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/reader/adapters')
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderBlogAdapter.java263
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderCommentAdapter.java497
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderMenuAdapter.java103
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java962
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderSearchSuggestionAdapter.java184
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java174
-rw-r--r--WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderUserAdapter.java112
7 files changed, 2295 insertions, 0 deletions
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderBlogAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderBlogAdapter.java
new file mode 100644
index 000000000..6c6257a86
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderBlogAdapter.java
@@ -0,0 +1,263 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+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.ReaderBlogList;
+import org.wordpress.android.models.ReaderRecommendBlogList;
+import org.wordpress.android.models.ReaderRecommendedBlog;
+import org.wordpress.android.ui.reader.ReaderInterfaces;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.StringUtils;
+import org.wordpress.android.util.UrlUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+import java.util.Collections;
+import java.util.Comparator;
+
+/*
+ * adapter which shows either recommended or followed blogs - used by ReaderBlogFragment
+ */
+public class ReaderBlogAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+ private static final int VIEW_TYPE_ITEM = 0;
+
+ public enum ReaderBlogType {RECOMMENDED, FOLLOWED}
+
+ public interface BlogClickListener {
+ void onBlogClicked(Object blog);
+ }
+
+ private final ReaderBlogType mBlogType;
+ private BlogClickListener mClickListener;
+ private ReaderInterfaces.DataLoadedListener mDataLoadedListener;
+
+ private ReaderRecommendBlogList mRecommendedBlogs = new ReaderRecommendBlogList();
+ private ReaderBlogList mFollowedBlogs = new ReaderBlogList();
+
+ @SuppressWarnings("UnusedParameters")
+ public ReaderBlogAdapter(Context context, ReaderBlogType blogType) {
+ super();
+ setHasStableIds(false);
+ mBlogType = blogType;
+ }
+
+ public void setDataLoadedListener(ReaderInterfaces.DataLoadedListener listener) {
+ mDataLoadedListener = listener;
+ }
+
+ public void setBlogClickListener(BlogClickListener listener) {
+ mClickListener = listener;
+ }
+
+ public void refresh() {
+ if (mIsTaskRunning) {
+ AppLog.w(T.READER, "load blogs task is already running");
+ return;
+ }
+ new LoadBlogsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ private ReaderBlogType getBlogType() {
+ return mBlogType;
+ }
+
+ public boolean isEmpty() {
+ return (getItemCount() == 0);
+ }
+
+ @Override
+ public int getItemCount() {
+ switch (getBlogType()) {
+ case RECOMMENDED:
+ return mRecommendedBlogs.size();
+ case FOLLOWED:
+ return mFollowedBlogs.size();
+ default:
+ return 0;
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return VIEW_TYPE_ITEM;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_ITEM:
+ View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.reader_listitem_blog, parent, false);
+ return new BlogViewHolder(itemView);
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ if (holder instanceof BlogViewHolder) {
+ final BlogViewHolder blogHolder = (BlogViewHolder) holder;
+ switch (getBlogType()) {
+ case RECOMMENDED:
+ final ReaderRecommendedBlog blog = mRecommendedBlogs.get(position);
+ blogHolder.txtTitle.setText(blog.getTitle());
+ blogHolder.txtDescription.setText(blog.getReason());
+ blogHolder.txtUrl.setText(UrlUtils.getHost(blog.getBlogUrl()));
+ blogHolder.imgBlog.setImageUrl(blog.getImageUrl(), WPNetworkImageView.ImageType.BLAVATAR);
+ break;
+
+ case FOLLOWED:
+ final ReaderBlog blogInfo = mFollowedBlogs.get(position);
+ if (blogInfo.hasName()) {
+ blogHolder.txtTitle.setText(blogInfo.getName());
+ } else {
+ blogHolder.txtTitle.setText(R.string.reader_untitled_post);
+ }
+ if (blogInfo.hasUrl()) {
+ blogHolder.txtUrl.setText(UrlUtils.getHost(blogInfo.getUrl()));
+ } else if (blogInfo.hasFeedUrl()) {
+ blogHolder.txtUrl.setText(UrlUtils.getHost(blogInfo.getFeedUrl()));
+ } else {
+ blogHolder.txtUrl.setText("");
+ }
+ blogHolder.imgBlog.setImageUrl(blogInfo.getImageUrl(), WPNetworkImageView.ImageType.BLAVATAR);
+ break;
+ }
+
+ if (mClickListener != null) {
+ blogHolder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int clickedPosition = blogHolder.getAdapterPosition();
+ switch (getBlogType()) {
+ case RECOMMENDED:
+ mClickListener.onBlogClicked(mRecommendedBlogs.get(clickedPosition));
+ break;
+ case FOLLOWED:
+ mClickListener.onBlogClicked(mFollowedBlogs.get(clickedPosition));
+ break;
+ }
+ }
+ });
+ }
+ }
+ }
+
+ /*
+ * holder used for followed/recommended blogs
+ */
+ class BlogViewHolder extends RecyclerView.ViewHolder {
+ private final TextView txtTitle;
+ private final TextView txtDescription;
+ private final TextView txtUrl;
+ private final WPNetworkImageView imgBlog;
+
+ public BlogViewHolder(View view) {
+ super(view);
+
+ txtTitle = (TextView) view.findViewById(R.id.text_title);
+ txtDescription = (TextView) view.findViewById(R.id.text_description);
+ txtUrl = (TextView) view.findViewById(R.id.text_url);
+ imgBlog = (WPNetworkImageView) view.findViewById(R.id.image_blog);
+
+ // followed blogs don't have a description
+ switch (getBlogType()) {
+ case FOLLOWED:
+ txtDescription.setVisibility(View.GONE);
+ break;
+ case RECOMMENDED:
+ txtDescription.setVisibility(View.VISIBLE);
+ break;
+ }
+ }
+ }
+
+ private boolean mIsTaskRunning = false;
+ private class LoadBlogsTask extends AsyncTask<Void, Void, Boolean> {
+ ReaderRecommendBlogList tmpRecommendedBlogs;
+ ReaderBlogList tmpFollowedBlogs;
+
+ @Override
+ protected void onPreExecute() {
+ mIsTaskRunning = true;
+ }
+
+ @Override
+ protected void onCancelled() {
+ mIsTaskRunning = false;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ switch (getBlogType()) {
+ case RECOMMENDED:
+ tmpRecommendedBlogs = ReaderBlogTable.getRecommendedBlogs();
+ return !mRecommendedBlogs.isSameList(tmpRecommendedBlogs);
+
+ case FOLLOWED:
+ tmpFollowedBlogs = ReaderBlogTable.getFollowedBlogs();
+ return !mFollowedBlogs.isSameList(tmpFollowedBlogs);
+
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result) {
+ switch (getBlogType()) {
+ case RECOMMENDED:
+ mRecommendedBlogs = (ReaderRecommendBlogList) tmpRecommendedBlogs.clone();
+ break;
+ case FOLLOWED:
+ mFollowedBlogs = (ReaderBlogList) tmpFollowedBlogs.clone();
+ // sort followed blogs by name/domain to match display
+ Collections.sort(mFollowedBlogs, new Comparator<ReaderBlog>() {
+ @Override
+ public int compare(ReaderBlog thisBlog, ReaderBlog thatBlog) {
+ String thisName = getBlogNameForComparison(thisBlog);
+ String thatName = getBlogNameForComparison(thatBlog);
+ return thisName.compareToIgnoreCase(thatName);
+ }
+ });
+ break;
+ }
+ notifyDataSetChanged();
+ }
+
+ mIsTaskRunning = false;
+
+ if (mDataLoadedListener != null) {
+ mDataLoadedListener.onDataLoaded(isEmpty());
+ }
+ }
+
+ private String getBlogNameForComparison(ReaderBlog blog) {
+ if (blog == null) {
+ return "";
+ } else if (blog.hasName()) {
+ return blog.getName();
+ } else if (blog.hasUrl()) {
+ return StringUtils.notNullStr(UrlUtils.getHost(blog.getUrl()));
+ } else {
+ return "";
+ }
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderCommentAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderCommentAdapter.java
new file mode 100644
index 000000000..8039d13f8
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderCommentAdapter.java
@@ -0,0 +1,497 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.os.AsyncTask;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.WordPress;
+import org.wordpress.android.datasets.ReaderCommentTable;
+import org.wordpress.android.datasets.ReaderPostTable;
+import org.wordpress.android.models.ReaderComment;
+import org.wordpress.android.models.ReaderCommentList;
+import org.wordpress.android.models.ReaderPost;
+import org.wordpress.android.ui.comments.CommentUtils;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderAnim;
+import org.wordpress.android.ui.reader.ReaderInterfaces;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderCommentActions;
+import org.wordpress.android.ui.reader.utils.ReaderLinkMovementMethod;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.ui.reader.views.ReaderCommentsPostHeaderView;
+import org.wordpress.android.ui.reader.views.ReaderIconCountView;
+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.ToastUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+public class ReaderCommentAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+ private ReaderPost mPost;
+ private boolean mMoreCommentsExist;
+
+ private static final int MAX_INDENT_LEVEL = 2;
+ private final int mIndentPerLevel;
+ private final int mAvatarSz;
+ private final int mContentWidth;
+
+ private long mHighlightCommentId = 0;
+ private boolean mShowProgressForHighlightedComment = false;
+ private final boolean mIsPrivatePost;
+ private final boolean mIsLoggedOutReader;
+ private boolean mIsHeaderClickEnabled;
+
+ private final int mColorAuthor;
+ private final int mColorNotAuthor;
+ private final int mColorHighlight;
+
+ private static final int VIEW_TYPE_HEADER = 1;
+ private static final int VIEW_TYPE_COMMENT = 2;
+
+ private static final long ID_HEADER = -1L;
+
+ private static final int NUM_HEADERS = 1;
+
+ public interface RequestReplyListener {
+ void onRequestReply(long commentId);
+ }
+
+ private ReaderCommentList mComments = new ReaderCommentList();
+ private RequestReplyListener mReplyListener;
+ private ReaderInterfaces.DataLoadedListener mDataLoadedListener;
+ private ReaderActions.DataRequestedListener mDataRequestedListener;
+
+ class CommentHolder extends RecyclerView.ViewHolder {
+ private final ViewGroup container;
+ private final TextView txtAuthor;
+ private final TextView txtText;
+ private final TextView txtDate;
+
+ private final WPNetworkImageView imgAvatar;
+ private final View spacerIndent;
+ private final ProgressBar progress;
+
+ private final TextView txtReply;
+ private final ImageView imgReply;
+
+ private final ReaderIconCountView countLikes;
+
+ public CommentHolder(View view) {
+ super(view);
+
+ container = (ViewGroup) view.findViewById(R.id.layout_container);
+
+ txtAuthor = (TextView) view.findViewById(R.id.text_comment_author);
+ txtText = (TextView) view.findViewById(R.id.text_comment_text);
+ txtDate = (TextView) view.findViewById(R.id.text_comment_date);
+
+ txtReply = (TextView) view.findViewById(R.id.text_comment_reply);
+ imgReply = (ImageView) view.findViewById(R.id.image_comment_reply);
+
+ imgAvatar = (WPNetworkImageView) view.findViewById(R.id.image_comment_avatar);
+ spacerIndent = view.findViewById(R.id.spacer_comment_indent);
+ progress = (ProgressBar) view.findViewById(R.id.progress_comment);
+
+ countLikes = (ReaderIconCountView) view.findViewById(R.id.count_likes);
+
+ txtText.setLinksClickable(true);
+ txtText.setMovementMethod(ReaderLinkMovementMethod.getInstance(mIsPrivatePost));
+ }
+ }
+
+ class PostHeaderHolder extends RecyclerView.ViewHolder {
+ private final ReaderCommentsPostHeaderView mHeaderView;
+
+ public PostHeaderHolder(View view) {
+ super(view);
+ mHeaderView = (ReaderCommentsPostHeaderView) view;
+ }
+ }
+
+ public ReaderCommentAdapter(Context context, ReaderPost post) {
+ mPost = post;
+ mIsPrivatePost = (post != null && post.isPrivate);
+ mIsLoggedOutReader = ReaderUtils.isLoggedOutReader();
+
+ mIndentPerLevel = context.getResources().getDimensionPixelSize(R.dimen.reader_comment_indent_per_level);
+ mAvatarSz = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_extra_small);
+
+ // calculate the max width of comment content
+ int displayWidth = DisplayUtils.getDisplayPixelWidth(context);
+ int cardMargin = context.getResources().getDimensionPixelSize(R.dimen.reader_card_margin);
+ int contentPadding = context.getResources().getDimensionPixelSize(R.dimen.reader_card_content_padding);
+ int mediumMargin = context.getResources().getDimensionPixelSize(R.dimen.margin_medium);
+ mContentWidth = displayWidth - (cardMargin * 2) - (contentPadding * 2) - (mediumMargin * 2);
+
+ mColorAuthor = ContextCompat.getColor(context, R.color.blue_medium);
+ mColorNotAuthor = ContextCompat.getColor(context, R.color.grey_dark);
+ mColorHighlight = ContextCompat.getColor(context, R.color.grey_lighten_30);
+
+ setHasStableIds(true);
+ }
+
+ public void setReplyListener(RequestReplyListener replyListener) {
+ mReplyListener = replyListener;
+ }
+
+ public void setDataLoadedListener(ReaderInterfaces.DataLoadedListener dataLoadedListener) {
+ mDataLoadedListener = dataLoadedListener;
+ }
+
+ public void setDataRequestedListener(ReaderActions.DataRequestedListener dataRequestedListener) {
+ mDataRequestedListener = dataRequestedListener;
+ }
+
+ public void enableHeaderClicks() {
+ mIsHeaderClickEnabled = true;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return position == 0 ? VIEW_TYPE_HEADER : VIEW_TYPE_COMMENT;
+ }
+
+ public void refreshComments() {
+ if (mIsTaskRunning) {
+ AppLog.w(T.READER, "reader comment adapter > Load comments task already running");
+ }
+ new LoadCommentsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mComments.size() + NUM_HEADERS;
+ }
+
+ public boolean isEmpty() {
+ return mComments.size() == 0;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_HEADER:
+ View headerView = new ReaderCommentsPostHeaderView(parent.getContext());
+ headerView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ return new PostHeaderHolder(headerView);
+ default:
+ View commentView = LayoutInflater.from(parent.getContext()).inflate(R.layout.reader_listitem_comment, parent, false);
+ return new CommentHolder(commentView);
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ if (holder instanceof PostHeaderHolder) {
+ PostHeaderHolder headerHolder = (PostHeaderHolder) holder;
+ headerHolder.mHeaderView.setPost(mPost);
+ if (mIsHeaderClickEnabled) {
+ headerHolder.mHeaderView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ReaderActivityLauncher.showReaderPostDetail(view.getContext(), mPost.blogId, mPost.postId);
+ }
+ });
+ }
+ return;
+ }
+
+ final ReaderComment comment = getItem(position);
+ if (comment == null) {
+ return;
+ }
+
+ CommentHolder commentHolder = (CommentHolder) holder;
+ commentHolder.txtAuthor.setText(comment.getAuthorName());
+
+ java.util.Date dtPublished = DateTimeUtils.dateFromIso8601(comment.getPublished());
+ commentHolder.txtDate.setText(DateTimeUtils.javaDateToTimeSpan(dtPublished, WordPress.getContext()));
+
+ if (comment.hasAuthorAvatar()) {
+ String avatarUrl = GravatarUtils.fixGravatarUrl(comment.getAuthorAvatar(), mAvatarSz);
+ commentHolder.imgAvatar.setImageUrl(avatarUrl, WPNetworkImageView.ImageType.AVATAR);
+ } else {
+ commentHolder.imgAvatar.showDefaultGravatarImage();
+ }
+
+ // tapping avatar or author name opens blog preview
+ if (comment.hasAuthorBlogId()) {
+ View.OnClickListener authorListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ReaderActivityLauncher.showReaderBlogPreview(
+ view.getContext(),
+ comment.authorBlogId
+ );
+ }
+ };
+ commentHolder.imgAvatar.setOnClickListener(authorListener);
+ commentHolder.txtAuthor.setOnClickListener(authorListener);
+ } else {
+ commentHolder.imgAvatar.setOnClickListener(null);
+ commentHolder.txtAuthor.setOnClickListener(null);
+ }
+
+ // author name uses different color for comments from the post's author
+ if (comment.authorId == mPost.authorId) {
+ commentHolder.txtAuthor.setTextColor(mColorAuthor);
+ } else {
+ commentHolder.txtAuthor.setTextColor(mColorNotAuthor);
+ }
+
+ // show indentation spacer for comments with parents and indent it based on comment level
+ int indentWidth;
+ if (comment.parentId != 0 && comment.level > 0) {
+ indentWidth = Math.min(MAX_INDENT_LEVEL, comment.level) * mIndentPerLevel;
+ RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) commentHolder.spacerIndent.getLayoutParams();
+ params.width = indentWidth;
+ commentHolder.spacerIndent.setVisibility(View.VISIBLE);
+ } else {
+ indentWidth = 0;
+ commentHolder.spacerIndent.setVisibility(View.GONE);
+ }
+
+ int maxImageWidth = mContentWidth - indentWidth;
+ CommentUtils.displayHtmlComment(commentHolder.txtText, comment.getText(), maxImageWidth);
+
+ // different background for highlighted comment, with optional progress bar
+ if (mHighlightCommentId != 0 && mHighlightCommentId == comment.commentId) {
+ commentHolder.container.setBackgroundColor(mColorHighlight);
+ commentHolder.progress.setVisibility(mShowProgressForHighlightedComment ? View.VISIBLE : View.GONE);
+ } else {
+ commentHolder.container.setBackgroundColor(Color.WHITE);
+ commentHolder.progress.setVisibility(View.GONE);
+ }
+
+ if (mIsLoggedOutReader) {
+ commentHolder.txtReply.setVisibility(View.GONE);
+ commentHolder.imgReply.setVisibility(View.GONE);
+ } else if (mReplyListener != null) {
+ // tapping reply icon tells activity to show reply box
+ View.OnClickListener replyClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mReplyListener.onRequestReply(comment.commentId);
+ }
+ };
+ commentHolder.txtReply.setOnClickListener(replyClickListener);
+ commentHolder.imgReply.setOnClickListener(replyClickListener);
+ }
+
+ showLikeStatus(commentHolder, position);
+
+ // if we're nearing the end of the comments and we know more exist on the server,
+ // fire request to load more
+ if (mMoreCommentsExist && mDataRequestedListener != null && (position >= getItemCount() - NUM_HEADERS)) {
+ mDataRequestedListener.onRequestData();
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ switch (getItemViewType(position)) {
+ case VIEW_TYPE_HEADER:
+ return ID_HEADER;
+ default:
+ ReaderComment comment = getItem(position);
+ return comment != null ? comment.commentId : 0;
+ }
+ }
+
+ private ReaderComment getItem(int position) {
+ return position == 0 ? null : mComments.get(position - NUM_HEADERS);
+ }
+
+ private void showLikeStatus(final CommentHolder holder, int position) {
+ ReaderComment comment = getItem(position);
+ if (comment == null) {
+ return;
+ }
+
+ if (mPost.canLikePost()) {
+ holder.countLikes.setVisibility(View.VISIBLE);
+ holder.countLikes.setSelected(comment.isLikedByCurrentUser);
+ holder.countLikes.setCount(comment.numLikes);
+
+ if (mIsLoggedOutReader) {
+ holder.countLikes.setEnabled(false);
+ } else {
+ holder.countLikes.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int clickedPosition = holder.getAdapterPosition();
+ toggleLike(v.getContext(), holder, clickedPosition);
+ }
+ });
+ }
+ } else {
+ holder.countLikes.setVisibility(View.GONE);
+ holder.countLikes.setOnClickListener(null);
+ }
+ }
+
+ private void toggleLike(Context context, CommentHolder holder, int position) {
+ if (!NetworkUtils.checkConnection(context)) {
+ return;
+ }
+
+ ReaderComment comment = getItem(position);
+ if (comment == null) {
+ ToastUtils.showToast(context, R.string.reader_toast_err_generic);
+ return;
+ }
+
+ boolean isAskingToLike = !comment.isLikedByCurrentUser;
+ ReaderAnim.animateLikeButton(holder.countLikes.getImageView(), isAskingToLike);
+
+ if (!ReaderCommentActions.performLikeAction(comment, isAskingToLike)) {
+ ToastUtils.showToast(context, R.string.reader_toast_err_generic);
+ return;
+ }
+
+ ReaderComment updatedComment = ReaderCommentTable.getComment(comment.blogId, comment.postId, comment.commentId);
+ if (updatedComment != null) {
+ mComments.set(position - NUM_HEADERS, updatedComment);
+ showLikeStatus(holder, position);
+ }
+ }
+
+ /*
+ * called from post detail activity when user submits a comment
+ */
+ public void addComment(ReaderComment comment) {
+ if (comment == null) {
+ return;
+ }
+
+ // if the comment doesn't have a parent we can just add it to the list of existing
+ // comments - but if it does have a parent, we need to reload the list so that it
+ // appears under its parent and is correctly indented
+ if (comment.parentId == 0) {
+ mComments.add(comment);
+ notifyDataSetChanged();
+ } else {
+ refreshComments();
+ }
+ }
+
+ /*
+ * called from post detail when submitted a comment fails - this removes the "fake" comment
+ * that was inserted while the API call was still being processed
+ */
+ public void removeComment(long commentId) {
+ if (commentId == mHighlightCommentId) {
+ setHighlightCommentId(0, false);
+ }
+
+ int index = mComments.indexOfCommentId(commentId);
+ if (index > -1) {
+ mComments.remove(index);
+ notifyDataSetChanged();
+ }
+ }
+
+ /*
+ * replace the comment that has the passed commentId with another comment
+ */
+ public void replaceComment(long commentId, ReaderComment comment) {
+ int position = positionOfCommentId(commentId);
+ if (position > -1 && mComments.replaceComment(commentId, comment)) {
+ notifyItemChanged(position);
+ }
+ }
+
+ /*
+ * sets the passed comment as highlighted with a different background color and an optional
+ * progress bar (used when posting new comments) - note that we don't call notifyDataSetChanged()
+ * here since in most cases it's unnecessary, so we leave it up to the caller to do that
+ */
+ public void setHighlightCommentId(long commentId, boolean showProgress) {
+ mHighlightCommentId = commentId;
+ mShowProgressForHighlightedComment = showProgress;
+ }
+
+ /*
+ * returns the position of the passed comment in the adapter, taking the header into account
+ */
+ public int positionOfCommentId(long commentId) {
+ int index = mComments.indexOfCommentId(commentId);
+ return index == -1 ? -1 : index + NUM_HEADERS;
+ }
+
+ /*
+ * AsyncTask to load comments for this post
+ */
+ private boolean mIsTaskRunning = false;
+
+ private class LoadCommentsTask extends AsyncTask<Void, Void, Boolean> {
+ private ReaderCommentList tmpComments;
+ private boolean tmpMoreCommentsExist;
+
+ @Override
+ protected void onPreExecute() {
+ mIsTaskRunning = true;
+ }
+
+ @Override
+ protected void onCancelled() {
+ mIsTaskRunning = false;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ if (mPost == null) {
+ return false;
+ }
+
+ // determine whether more comments can be downloaded by comparing the number of
+ // comments the post says it has with the number of comments actually stored
+ // locally for this post
+ int numServerComments = ReaderPostTable.getNumCommentsForPost(mPost);
+ int numLocalComments = ReaderCommentTable.getNumCommentsForPost(mPost);
+ tmpMoreCommentsExist = (numServerComments > numLocalComments);
+
+ tmpComments = ReaderCommentTable.getCommentsForPost(mPost);
+ return !mComments.isSameList(tmpComments);
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ mMoreCommentsExist = tmpMoreCommentsExist;
+
+ if (result) {
+ // assign the comments with children sorted under their parents and indent levels applied
+ mComments = ReaderCommentList.getLevelList(tmpComments);
+ notifyDataSetChanged();
+ }
+ if (mDataLoadedListener != null) {
+ mDataLoadedListener.onDataLoaded(isEmpty());
+ }
+ mIsTaskRunning = false;
+ }
+ }
+
+ /*
+ * Set a post to adapter and update relevant information in the post header
+ */
+ public void setPost(ReaderPost post) {
+ if (post != null) {
+ mPost = post;
+ notifyItemChanged(0); //notify header to update itself
+ }
+
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderMenuAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderMenuAdapter.java
new file mode 100644
index 000000000..85bfab181
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderMenuAdapter.java
@@ -0,0 +1,103 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/*
+ * adapter for the popup menu that appears when clicking "..." in the reader
+ */
+public class ReaderMenuAdapter extends BaseAdapter {
+
+ private final LayoutInflater mInflater;
+ private final List<Integer> mMenuItems = new ArrayList<>();
+
+ public static final int ITEM_FOLLOW = 0;
+ public static final int ITEM_UNFOLLOW = 1;
+ public static final int ITEM_BLOCK = 2;
+
+ public ReaderMenuAdapter(Context context, @NonNull List<Integer> menuItems) {
+ super();
+ mInflater = LayoutInflater.from(context);
+ mMenuItems.addAll(menuItems);
+ }
+
+ @Override
+ public int getCount() {
+ return mMenuItems.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mMenuItems.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mMenuItems.get(position);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ReaderMenuHolder holder;
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.reader_popup_menu_item, parent, false);
+ holder = new ReaderMenuHolder(convertView);
+ convertView.setTag(holder);
+ } else {
+ holder = (ReaderMenuHolder) convertView.getTag();
+ }
+
+ int textRes;
+ int textColorRes;
+ int iconRes;
+ switch (mMenuItems.get(position)) {
+ case ITEM_FOLLOW:
+ textRes = R.string.reader_btn_follow;
+ textColorRes = R.color.reader_follow;
+ iconRes = R.drawable.reader_follow;
+ break;
+ case ITEM_UNFOLLOW:
+ textRes = R.string.reader_btn_unfollow;
+ textColorRes = R.color.reader_following;
+ iconRes = R.drawable.reader_following;
+ break;
+ case ITEM_BLOCK:
+ textRes = R.string.reader_menu_block_blog;
+ textColorRes = R.color.grey_dark;
+ iconRes = 0;
+ break;
+ default:
+ return convertView;
+ }
+
+ holder.text.setText(textRes);
+ holder.text.setTextColor(convertView.getContext().getResources().getColor(textColorRes));
+
+ if (iconRes != 0) {
+ holder.icon.setImageResource(iconRes);
+ }
+
+ return convertView;
+ }
+
+ class ReaderMenuHolder {
+ private final TextView text;
+ private final ImageView icon;
+
+ ReaderMenuHolder(View view) {
+ text = (TextView) view.findViewById(R.id.text);
+ icon = (ImageView) view.findViewById(R.id.image);
+ }
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java
new file mode 100644
index 000000000..e650bc22b
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java
@@ -0,0 +1,962 @@
+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;
+ }
+ }
+} \ No newline at end of file
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderSearchSuggestionAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderSearchSuggestionAdapter.java
new file mode 100644
index 000000000..44228bb58
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderSearchSuggestionAdapter.java
@@ -0,0 +1,184 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.widget.CursorAdapter;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderSearchTable;
+
+public class ReaderSearchSuggestionAdapter extends CursorAdapter {
+ private static final int MAX_SUGGESTIONS = 5;
+ private static final int CLEAR_ALL_ROW_ID = -1;
+
+ private static final int NUM_VIEW_TYPES = 2;
+ private static final int VIEW_TYPE_QUERY = 0;
+ private static final int VIEW_TYPE_CLEAR = 1;
+
+ private String mCurrentFilter;
+ private final Object[] mClearAllRow;
+ private final int mClearAllBgColor;
+ private final int mSuggestionBgColor;
+
+ public ReaderSearchSuggestionAdapter(Context context) {
+ super(context, null, false);
+ String clearAllText = context.getString(R.string.label_clear_search_history);
+ mClearAllRow = new Object[]{CLEAR_ALL_ROW_ID, clearAllText};
+ mClearAllBgColor = ContextCompat.getColor(context, R.color.grey_lighten_30);
+ mSuggestionBgColor = ContextCompat.getColor(context, R.color.filtered_list_suggestions);
+ }
+
+ public void setFilter(String filter) {
+ // skip if unchanged
+ if (isCurrentFilter(filter) && getCursor() != null) {
+ return;
+ }
+
+ // get db cursor containing matching query strings
+ Cursor sqlCursor = ReaderSearchTable.getQueryStringCursor(filter, MAX_SUGGESTIONS);
+
+ // create a MatrixCursor which will be the actual cursor behind this adapter
+ MatrixCursor matrixCursor = new MatrixCursor(
+ new String[]{
+ ReaderSearchTable.COL_ID,
+ ReaderSearchTable.COL_QUERY});
+
+ if (sqlCursor.moveToFirst()) {
+ // first populate the matrix from the db cursor...
+ do {
+ long id = sqlCursor.getLong(sqlCursor.getColumnIndex(ReaderSearchTable.COL_ID));
+ String query = sqlCursor.getString(sqlCursor.getColumnIndex(ReaderSearchTable.COL_QUERY));
+ matrixCursor.addRow(new Object[]{id, query});
+ } while (sqlCursor.moveToNext());
+
+ // ...then add our custom item
+ matrixCursor.addRow(mClearAllRow);
+ }
+
+ mCurrentFilter = filter;
+ swapCursor(matrixCursor);
+ }
+
+ /*
+ * forces setFilter() to always repopulate by skipping the isCurrentFilter() check
+ */
+ private void reload() {
+ String newFilter = mCurrentFilter;
+ mCurrentFilter = null;
+ setFilter(newFilter);
+ }
+
+ private boolean isCurrentFilter(String filter) {
+ if (TextUtils.isEmpty(filter) && TextUtils.isEmpty(mCurrentFilter)) {
+ return true;
+ }
+ return filter != null && filter.equalsIgnoreCase(mCurrentFilter);
+ }
+
+ public String getSuggestion(int position) {
+ Cursor cursor = (Cursor) getItem(position);
+ if (cursor != null) {
+ return cursor.getString(cursor.getColumnIndex(ReaderSearchTable.COL_QUERY));
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ // use a different view type for the "clear" row so it doesn't get recycled and used
+ // as a query row
+ if (getItemId(position) == CLEAR_ALL_ROW_ID) {
+ return VIEW_TYPE_CLEAR;
+ }
+ return VIEW_TYPE_QUERY;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return NUM_VIEW_TYPES;
+ }
+
+ private class SuggestionViewHolder {
+ private final TextView txtSuggestion;
+ private final ImageView imgDelete;
+
+ SuggestionViewHolder(View view) {
+ txtSuggestion = (TextView) view.findViewById(R.id.text_suggestion);
+ imgDelete = (ImageView) view.findViewById(R.id.image_delete);
+ }
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View view = LayoutInflater.from(context).inflate(R.layout.reader_listitem_suggestion, parent, false);
+
+ SuggestionViewHolder holder = new SuggestionViewHolder(view);
+ view.setTag(holder);
+
+ long id = cursor.getLong(cursor.getColumnIndex(ReaderSearchTable.COL_ID));
+ if (id == CLEAR_ALL_ROW_ID) {
+ view.setBackgroundColor(mClearAllBgColor);
+ view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ confirmClearSavedSearches(v.getContext());
+ }
+ });
+ holder.imgDelete.setVisibility(View.GONE);
+ } else {
+ view.setBackgroundColor(mSuggestionBgColor);
+ }
+
+ return view;
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ SuggestionViewHolder holder = (SuggestionViewHolder) view.getTag();
+
+ final String query = cursor.getString(cursor.getColumnIndex(ReaderSearchTable.COL_QUERY));
+ holder.txtSuggestion.setText(query);
+
+ long id = cursor.getLong(cursor.getColumnIndex(ReaderSearchTable.COL_ID));
+ if (id != CLEAR_ALL_ROW_ID) {
+ holder.imgDelete.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ReaderSearchTable.deleteQueryString(query);
+ reload();
+ }
+ });
+ }
+ }
+
+ private void confirmClearSavedSearches(Context context) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(R.string.dlg_confirm_clear_search_history)
+ .setCancelable(true)
+ .setNegativeButton(R.string.no, null)
+ .setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ clearSavedSearches();
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+
+ private void clearSavedSearches() {
+ ReaderSearchTable.deleteAllQueries();
+ swapCursor(null);
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java
new file mode 100644
index 000000000..de203e0a3
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderTagAdapter.java
@@ -0,0 +1,174 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.datasets.ReaderTagTable;
+import org.wordpress.android.models.ReaderTag;
+import org.wordpress.android.models.ReaderTagList;
+import org.wordpress.android.ui.reader.ReaderInterfaces;
+import org.wordpress.android.ui.reader.actions.ReaderActions;
+import org.wordpress.android.ui.reader.actions.ReaderTagActions;
+import org.wordpress.android.ui.reader.utils.ReaderUtils;
+import org.wordpress.android.util.AppLog;
+import org.wordpress.android.util.AppLog.T;
+import org.wordpress.android.util.NetworkUtils;
+import org.wordpress.android.util.ToastUtils;
+
+import java.lang.ref.WeakReference;
+
+public class ReaderTagAdapter extends RecyclerView.Adapter<ReaderTagAdapter.TagViewHolder> {
+
+ public interface TagDeletedListener {
+ void onTagDeleted(ReaderTag tag);
+ }
+
+ private final WeakReference<Context> mWeakContext;
+ private final ReaderTagList mTags = new ReaderTagList();
+ private TagDeletedListener mTagDeletedListener;
+ private ReaderInterfaces.DataLoadedListener mDataLoadedListener;
+
+ public ReaderTagAdapter(Context context) {
+ super();
+ setHasStableIds(true);
+ mWeakContext = new WeakReference<>(context);
+ }
+
+ public void setTagDeletedListener(TagDeletedListener listener) {
+ mTagDeletedListener = listener;
+ }
+
+ public void setDataLoadedListener(ReaderInterfaces.DataLoadedListener listener) {
+ mDataLoadedListener = listener;
+ }
+
+ private boolean hasContext() {
+ return (getContext() != null);
+ }
+
+ private Context getContext() {
+ return mWeakContext.get();
+ }
+
+ public void refresh() {
+ if (mIsTaskRunning) {
+ AppLog.w(T.READER, "tag task is already running");
+ return;
+ }
+ new LoadTagsTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mTags.size();
+ }
+
+ public boolean isEmpty() {
+ return (getItemCount() == 0);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mTags.get(position).getTagSlug().hashCode();
+ }
+
+ @Override
+ public TagViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.reader_listitem_tag, parent, false);
+ return new TagViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(TagViewHolder holder, int position) {
+ final ReaderTag tag = mTags.get(position);
+ holder.txtTagName.setText(tag.getLabel());
+ holder.btnRemove.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ performDeleteTag(tag);
+
+ }
+ });
+ }
+
+ private void performDeleteTag(@NonNull ReaderTag tag) {
+ if (!NetworkUtils.checkConnection(getContext())) {
+ return;
+ }
+
+ ReaderActions.ActionListener actionListener = new ReaderActions.ActionListener() {
+ @Override
+ public void onActionResult(boolean succeeded) {
+ if (!succeeded && hasContext()) {
+ ToastUtils.showToast(getContext(), R.string.reader_toast_err_remove_tag);
+ refresh();
+ }
+ }
+ };
+
+ boolean success = ReaderTagActions.deleteTag(tag, actionListener);
+
+ if (success) {
+ int index = mTags.indexOfTagName(tag.getTagSlug());
+ if (index > -1) {
+ mTags.remove(index);
+ notifyItemRemoved(index);
+ }
+ if (mTagDeletedListener != null) {
+ mTagDeletedListener.onTagDeleted(tag);
+ }
+ }
+ }
+
+ class TagViewHolder extends RecyclerView.ViewHolder {
+ private final TextView txtTagName;
+ private final ImageButton btnRemove;
+
+ public TagViewHolder(View view) {
+ super(view);
+ txtTagName = (TextView) view.findViewById(R.id.text_topic);
+ btnRemove = (ImageButton) view.findViewById(R.id.btn_remove);
+ ReaderUtils.setBackgroundToRoundRipple(btnRemove);
+ }
+ }
+
+ /*
+ * AsyncTask to load tags
+ */
+ private boolean mIsTaskRunning = false;
+ private class LoadTagsTask extends AsyncTask<Void, Void, ReaderTagList> {
+ @Override
+ protected void onPreExecute() {
+ mIsTaskRunning = true;
+ }
+ @Override
+ protected void onCancelled() {
+ mIsTaskRunning = false;
+ }
+ @Override
+ protected ReaderTagList doInBackground(Void... params) {
+ return ReaderTagTable.getFollowedTags();
+ }
+ @Override
+ protected void onPostExecute(ReaderTagList tagList) {
+ if (tagList != null && !tagList.isSameList(mTags)) {
+ mTags.clear();
+ mTags.addAll(tagList);
+ notifyDataSetChanged();
+ }
+ mIsTaskRunning = false;
+ if (mDataLoadedListener != null) {
+ mDataLoadedListener.onDataLoaded(isEmpty());
+ }
+ }
+ }
+
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderUserAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderUserAdapter.java
new file mode 100644
index 000000000..00e6ce023
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderUserAdapter.java
@@ -0,0 +1,112 @@
+package org.wordpress.android.ui.reader.adapters;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.wordpress.android.R;
+import org.wordpress.android.models.ReaderUser;
+import org.wordpress.android.models.ReaderUserList;
+import org.wordpress.android.ui.reader.ReaderActivityLauncher;
+import org.wordpress.android.ui.reader.ReaderInterfaces.DataLoadedListener;
+import org.wordpress.android.util.GravatarUtils;
+import org.wordpress.android.widgets.WPNetworkImageView;
+
+/**
+ * owner must call setUsers() with the list of
+ * users to display
+ */
+public class ReaderUserAdapter extends RecyclerView.Adapter<ReaderUserAdapter.UserViewHolder> {
+ private final ReaderUserList mUsers = new ReaderUserList();
+ private DataLoadedListener mDataLoadedListener;
+ private final int mAvatarSz;
+
+ public ReaderUserAdapter(Context context) {
+ super();
+ mAvatarSz = context.getResources().getDimensionPixelSize(R.dimen.avatar_sz_small);
+ setHasStableIds(true);
+ }
+
+ public void setDataLoadedListener(DataLoadedListener listener) {
+ mDataLoadedListener = listener;
+ }
+
+ @Override
+ public int getItemCount() {
+ return mUsers.size();
+ }
+
+ private boolean isEmpty() {
+ return (getItemCount() == 0);
+ }
+
+ @Override
+ public UserViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.reader_listitem_user, parent, false);
+ return new UserViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(UserViewHolder holder, int position) {
+ final ReaderUser user = mUsers.get(position);
+
+ holder.txtName.setText(user.getDisplayName());
+ if (user.hasUrl()) {
+ holder.txtUrl.setVisibility(View.VISIBLE);
+ holder.txtUrl.setText(user.getUrlDomain());
+ holder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (user.hasBlogId()) {
+ ReaderActivityLauncher.showReaderBlogPreview(
+ v.getContext(),
+ user.blogId);
+ }
+ }
+ });
+ } else {
+ holder.txtUrl.setVisibility(View.GONE);
+ holder.itemView.setOnClickListener(null);
+ }
+
+ if (user.hasAvatarUrl()) {
+ holder.imgAvatar.setImageUrl(
+ GravatarUtils.fixGravatarUrl(user.getAvatarUrl(), mAvatarSz),
+ WPNetworkImageView.ImageType.AVATAR);
+ } else {
+ holder.imgAvatar.showDefaultGravatarImage();
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mUsers.get(position).userId;
+ }
+
+ class UserViewHolder extends RecyclerView.ViewHolder {
+ private final TextView txtName;
+ private final TextView txtUrl;
+ private final WPNetworkImageView imgAvatar;
+
+ public UserViewHolder(View view) {
+ super(view);
+ txtName = (TextView) view.findViewById(R.id.text_name);
+ txtUrl = (TextView) view.findViewById(R.id.text_url);
+ imgAvatar = (WPNetworkImageView) view.findViewById(R.id.image_avatar);
+ }
+ }
+
+ public void setUsers(final ReaderUserList users) {
+ mUsers.clear();
+ if (users != null && users.size() > 0) {
+ mUsers.addAll(users);
+ }
+ notifyDataSetChanged();
+ if (mDataLoadedListener != null) {
+ mDataLoadedListener.onDataLoaded(isEmpty());
+ }
+ }
+}