diff options
author | Chris Warrington <cmw@google.com> | 2016-10-18 12:29:21 +0100 |
---|---|---|
committer | Chris Warrington <cmw@google.com> | 2016-10-18 12:34:18 +0100 |
commit | e3780081075c01aa1dff6d1f373cb43192b33e68 (patch) | |
tree | fb734615933a39f3d009210dc0d1457160479b35 /WordPress/src/main/java/org/wordpress/android/ui/reader/adapters | |
parent | 7e05eb7e57827eddc885570bc00aed8a50320dbf (diff) | |
parent | 025b8b226c8d8edba2b309ca878572f40512eca7 (diff) | |
download | gradle-perf-android-medium-main.tar.gz |
Merge remote-tracking branch 'origin/upstream-master' into masterHEADstudio-3.4.0studio-3.2.1studio-3.1.2studio-3.0studio-2.3gradle_3.4.0gradle_3.1.2gradle_3.0.0gradle_2.3.0studio-master-devmirror-goog-studio-master-devmastermain
Change-Id: I63f5e16d09297c48432192761b840310935eb903
Diffstat (limited to 'WordPress/src/main/java/org/wordpress/android/ui/reader/adapters')
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()); + } + } +} |